/* =============================================================================
   JJHA Family Ops — design system
   =============================================================================
   Deliberately not a SaaS admin dashboard. This is read by a household staff
   of six across four properties, often on a phone, often mid-task. It should
   feel like a well-kept house: calm, legible, unhurried, nothing shouting.

   Warm neutrals rather than the usual cool greys, generous whitespace, one
   restrained accent, and a serif for headings to keep it from reading as
   software.
   ========================================================================== */

:root {
  --ink:            #1c1a17;
  --ink-soft:       #56504a;
  --ink-faint:      #8a827a;
  --paper:          #faf8f5;
  --paper-raised:   #ffffff;
  --line:           #e6e0d8;
  --line-soft:      #f0ebe4;
  --accent:         #2c5f8a;
  --accent-soft:    #eaf1f7;
  --warn:           #9a5b25;
  --warn-soft:      #fbf1e7;
  --good:           #3d6b4a;
  --good-soft:      #edf3ee;

  --serif: "Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua",
           Georgia, serif;
  --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
          "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;

  --radius: 10px;
  --shadow: 0 1px 2px rgba(28, 26, 23, 0.04),
            0 4px 16px rgba(28, 26, 23, 0.04);
}

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

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

body {
  font-family: var(--sans);
  background: var(--paper);
  color: var(--ink);
  line-height: 1.55;
  font-size: 15px;
  /* dvh where supported: 100vh includes the ground iOS Safari's URL bar is
     standing on, so the shell ran ~90px taller than the visible screen. */
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}

a { color: inherit; text-decoration: none; }

/* ---------- masthead ---------- */

.masthead {
  border-bottom: 1px solid var(--line);
  background: var(--paper-raised);
  position: sticky;
  top: 0;
  z-index: 20;
}

.masthead-inner {
  max-width: 1180px;
  margin: 0 auto;
  padding: 0 24px;
  height: 62px;
  display: flex;
  align-items: center;
  gap: 28px;
}

.wordmark {
  font-family: var(--serif);
  font-size: 1.16rem;
  letter-spacing: 0.01em;
  white-space: nowrap;
}
.wordmark span { color: var(--ink-faint); font-style: italic; }

.nav { display: flex; gap: 4px; flex: 1; align-items: center; }
.nav::-webkit-scrollbar { display: none; }

.nav a {
  padding: 7px 12px;
  border-radius: 7px;
  font-size: 0.875rem;
  color: var(--ink-soft);
  white-space: nowrap;
  transition: background 0.12s, color 0.12s;
}
.nav a:hover { background: var(--line-soft); color: var(--ink); }
.nav a.active { background: var(--accent-soft); color: var(--accent); font-weight: 500; }

/* ---------- grouped nav ----------
   No JavaScript: the menu opens on hover and on keyboard focus anywhere
   inside the group, which covers pointer and tab. `overflow-x: auto` on .nav
   would clip an absolutely positioned menu, so the scroll behaviour moves to
   a wrapper and .nav itself is allowed to overflow visibly.

   The group label is a real link to the section's main page. A menu is a
   shortcut, never the only route: hover fails on touch, and a pointer that
   leaves by two pixels should not strand anyone. */

.navgroup { position: relative; display: inline-block; }
.navgroup > a { display: inline-flex; align-items: center; gap: 4px; }
.navgroup .caret { font-size: 0.65em; opacity: 0.55; }

.navmenu {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  min-width: 208px;
  padding: 6px;
  background: var(--bg-raised, #fff);
  border: 1px solid var(--line);
  border-radius: 10px;
  box-shadow: 0 12px 28px rgba(15, 23, 42, 0.12);
  display: none;
  z-index: 40;
}

.navgroup:hover .navmenu,
.navgroup:focus-within .navmenu { display: block; }

.navsep { height: 1px; background: var(--line-soft); margin: 5px 8px; }

.navmenu a {
  display: block;
  padding: 7px 10px;
  border-radius: 6px;
  font-size: 0.85rem;
  white-space: nowrap;
}

/* A hover bridge. Without it the 6px gap between the label and the menu is a
   dead room that closes the menu on the way down. */
.navgroup::after {
  content: "";
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  height: 8px;
}

.who { display: flex; align-items: center; gap: 10px; white-space: nowrap; }
.who img { width: 27px; height: 27px; border-radius: 50%; }
.who-name { font-size: 0.83rem; color: var(--ink-soft); }
.who a { font-size: 0.8rem; color: var(--ink-faint); }
.who a:hover { color: var(--ink); }

/* ---------- the application switcher ----------
   Two applications for one household, and until now no route between them but
   typing a hostname. The name in the masthead becomes a disclosure holding
   both, with the one you are standing in marked as current — THE OWNER'S ASK,
   27 Aug 2026, and a deliberate partial reversal of the 24 Aug call that
   turned the name back into a plain link. That reversion was about the
   LEARNING doors: they had moved to the Help menu, so the name's menu was left
   holding nothing anybody needed. This one holds the only door to the other
   application, and it is offered to PRINCIPALS ONLY in both shells — which is
   who the office admits — so the clutter argument still governs everyone it
   was made about, who keep the plain link.

   <details> rather than the .navgroup pattern above, which opens on hover.
   Hover fails on touch and this is a control a phone has to reach; a
   disclosure is native, opens on a tap, needs no script and closes on a second
   one. `_appswitch.html` carries a few lines beside it that tidy up after it
   — chosen link, click outside, Escape — and where they do not run nothing is
   lost.

   Its own classes rather than .navmenu, deliberately: the phone rules force
   every .navmenu open and static so the groups read as headings inside the
   hamburger panel, and that rule once reached the name's menu and rendered it
   permanently expanded under the header on every phone page. The correction is
   still in this file, at `.who .navgroup .navmenu`; a second component reusing
   the class would need a second one. */

.appswitch { position: relative; }

.appswitch > summary {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 34px;
  padding: 3px 6px;
  border-radius: 8px;
  cursor: pointer;
  /* Both halves are needed to lose the disclosure triangle: `list-style` for
     everyone, the pseudo-element for WebKit. */
  list-style: none;
  -webkit-tap-highlight-color: transparent;
}
.appswitch > summary::-webkit-details-marker { display: none; }

.appswitch > summary:hover,
.appswitch[open] > summary { background: var(--line-soft); }

/* Explicit, because the masthead sets no colour of its own: every other label
   up there carries one, and an inherited caret is invisible against the
   office's dark bar. */
.appswitch .caret { font-size: 0.65em; color: var(--ink-faint); }

/* The avatar sits INSIDE the summary, which is what keeps the control hittable
   on a phone: `.who-name` is display:none below 720px, and without the picture
   the trigger would shrink to a caret. */

.appmenu {
  position: absolute;
  top: calc(100% + 6px);
  /* Right-aligned: the cluster is the last thing in the masthead, so a menu
     anchored left would hang off the edge of the window. */
  right: 0;
  min-width: 234px;
  padding: 6px;
  background: var(--paper-raised, #fff);
  border: 1px solid var(--line);
  border-radius: 10px;
  box-shadow: 0 12px 28px rgba(15, 23, 42, 0.12);
  z-index: 60;
}

.appmenu a,
.appmenu .appmenu-here {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 9px 10px;
  border-radius: 7px;
  font-size: 0.85rem;
  white-space: nowrap;
}
.appmenu a { color: var(--ink); }
.appmenu a:hover { background: var(--line-soft); color: var(--ink); }
.appmenu .appmenu-here { color: var(--ink-faint); cursor: default; }
.appmenu .appmenu-tag { font-size: 0.72rem; color: var(--ink-faint); font-weight: 400; }

/* On a phone the menu leaves the control's box and spans the window.

   Not a preference — a right-anchored menu is only on-screen while the control
   it hangs from is near the right edge, and the two mastheads disagree about
   that at this width. HouseOps keeps the whole cluster on one row; the office
   wordmark is longer, so `.who` wraps to a second row at the LEFT and the same
   `right: 0` put a 234px menu almost entirely off the left of the screen.
   Found by looking at it, not by reading it.

   `position: static` on the disclosure hands the menu back to the initial
   containing block, so `left`/`right` measure the window while `top: auto`
   keeps its STATIC position — which is exactly where it already was, under the
   summary. One rule, no measurement, and it cannot be wrong about a masthead
   that rearranges itself again later. */
@media (max-width: 720px) {
  .appswitch { position: static; }
  .appmenu {
    left: 12px;
    right: 12px;
    top: auto;
    margin-top: 6px;
    min-width: 0;
  }
}

/* ---------- layout ---------- */

.wrap { max-width: 1180px; margin: 0 auto; padding: 36px 24px 72px; width: 100%; flex: 1; }

.page-head { margin-bottom: 28px; }
.page-head h1 {
  font-family: var(--serif);
  font-size: 1.95rem;
  font-weight: 400;
  letter-spacing: -0.01em;
  line-height: 1.2;
}
.page-head .sub { color: var(--ink-faint); font-size: 0.92rem; margin-top: 5px; }

.crumb { font-size: 0.82rem; color: var(--ink-faint); margin-bottom: 9px; }
.crumb a:hover { color: var(--accent); }
.crumb span { margin: 0 6px; opacity: 0.5; }

.section { margin-top: 40px; }
.section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: 14px;
  padding-bottom: 9px;
  border-bottom: 1px solid var(--line);
}
.section-head h2 {
  font-family: var(--serif);
  font-size: 1.14rem;
  font-weight: 400;
}
.section-head .meta { font-size: 0.8rem; color: var(--ink-faint); }

/* A section that is real but secondary — provenance somebody looks for once
   in a while, next to facts somebody opens the page to find. Smaller and
   without the card's own border, rather than hidden: still above the fold,
   just visibly the second thing here rather than competing with the first. */
.section.subtle .section-head {
  margin-bottom: 8px; padding-bottom: 5px; border-bottom: 1px solid var(--line-soft);
}
.section.subtle .section-head h2 { font-size: 0.82rem; color: var(--ink-faint); font-family: var(--sans); }
.facts.subtle {
  background: none; border: none; box-shadow: none;
}
.facts.subtle .fact {
  padding: 5px 0; border-bottom: 1px solid var(--line-soft); font-size: 0.8rem;
}
.facts.subtle .fact dt { color: var(--ink-faint); font-size: 0.68rem; }
.facts.subtle .fact dd { color: var(--ink-soft); }

/* ---------- cards ---------- */

.grid { display: grid; gap: 14px; }
.grid.two   { grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); }
.grid.three { grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); }
.grid.four  { grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); }

/* The digest number row. auto-fit rather than auto-fill, and a smaller floor,
   so six counts sit on one line instead of five plus an orphan — and so a role
   that only earns three of them gets three wide cards rather than three narrow
   ones with a gap. */
.grid.stats { grid-template-columns: repeat(auto-fit, minmax(148px, 1fr)); }

/* ---------- digest ----------
   The counts were six cards a screen tall, which spent the whole fold saying
   "there are four properties". One line instead, and the three queues sit
   beside each other underneath so the actionable part is visible without
   scrolling. */

.statstrip {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 22px;
  padding: 12px 16px;
  margin-bottom: 22px;
  background: var(--bg-raised, #fff);
  border: 1px solid var(--line);
  border-radius: 10px;
  font-size: 0.86rem;
  color: var(--ink-soft);
}
.statstrip a { color: var(--ink-soft); white-space: nowrap; }
.statstrip a:hover { color: var(--ink); }
.statstrip b { font-size: 1.05rem; color: var(--ink); font-weight: 600; margin-right: 3px; }

.grid.digest { align-items: start; margin-bottom: 8px; }
.grid.digest .section { margin: 0; }

ul.rows { list-style: none; margin: 0; padding: 0; }
ul.rows li {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 8px;
  padding: 7px 0;
  border-bottom: 1px solid var(--line-soft);
  font-size: 0.87rem;
}
ul.rows li:last-child { border-bottom: 0; }
ul.rows .dim { font-size: 0.82rem; }


.card {
  background: var(--paper-raised);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 18px;
  box-shadow: var(--shadow);
  transition: border-color 0.14s, transform 0.14s;
}
a.card:hover { border-color: var(--accent); transform: translateY(-1px); }

.card h3 { font-family: var(--serif); font-size: 1.06rem; font-weight: 400; margin-bottom: 4px; }
.card p { font-size: 0.86rem; color: var(--ink-soft); }

.card.muted { opacity: 0.55; }
.card.muted:hover { transform: none; border-color: var(--line); }

/* ---------- stats ---------- */

.stat { text-align: left; }
.stat .n {
  font-family: var(--serif);
  font-size: 2.05rem;
  line-height: 1.05;
  letter-spacing: -0.02em;
}
.stat .k {
  font-size: 0.73rem;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--ink-faint);
  margin-top: 5px;
}

/* ---------- tables ---------- */

.table-wrap {
  background: var(--paper-raised);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  /* -x auto rather than hidden: a `table.keep` matrix inside must be able to
     scroll sideways on a phone. When nothing overflows this still clips the
     corners exactly as `hidden` did. */
  overflow-x: auto;
  overflow-y: hidden;
  box-shadow: var(--shadow);
}

table { width: 100%; border-collapse: collapse; font-size: 0.88rem; }
thead th {
  text-align: left;
  font-weight: 500;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--ink-faint);
  padding: 11px 16px;
  border-bottom: 1px solid var(--line);
  background: var(--paper);
}
tbody td { padding: 12px 16px; border-bottom: 1px solid var(--line-soft); vertical-align: top; }
tbody tr:last-child td { border-bottom: none; }
tbody tr:hover { background: var(--paper); }
td .dim { color: var(--ink-faint); font-size: 0.82rem; }
td .mono { font-family: var(--mono); font-size: 0.79rem; color: var(--ink-soft); }

/* ---------- pills ---------- */

.pill {
  display: inline-block;
  padding: 2px 9px;
  border-radius: 20px;
  font-size: 0.73rem;
  font-weight: 500;
  background: var(--line-soft);
  color: var(--ink-soft);
  white-space: nowrap;
}
.pill.accent { background: var(--accent-soft); color: var(--accent); }
.pill.good   { background: var(--good-soft); color: var(--good); }
.pill.warn   { background: var(--warn-soft); color: var(--warn); }

/* ---------- photographs ---------- */

.plates { display: grid; gap: 16px; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); }

.plate {
  background: var(--paper-raised);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow);
}
.plate img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  display: block;
  background: var(--line-soft);
}
a.plate { display: block; transition: border-color 0.14s, transform 0.14s; }
a.plate:hover { border-color: var(--accent); transform: translateY(-1px); }

.plate .cap { padding: 12px 14px; }
.plate .cap h4 { font-size: 0.9rem; font-weight: 500; margin-bottom: 3px; }
.plate .cap p { font-size: 0.8rem; color: var(--ink-faint); }

/* ---------- thumbnails in lists ---------- */
/* A row with a photograph is scanned differently from a row without one: the
   eye finds the object before it reads the name. Small, fixed, and never
   reflowing when an image is missing. */

/* The frame is a span and the photograph sits inside it, so an image that
   fails to load can simply hide and leave the frame — no reflow, and no
   broken-image glyph pretending the page is broken. */
.thumb {
  width: 46px;
  height: 46px;
  border-radius: 7px;
  display: block;
  overflow: hidden;
  background: var(--line-soft);
  border: 1px solid var(--line);
}
.thumb.empty-thumb { border-style: dashed; }
.thumb-img { width: 100%; height: 100%; object-fit: cover; display: block; }

td.thumb-cell { width: 62px; padding-right: 0; }

.rowlink { display: block; color: inherit; }
.rowlink:hover { color: var(--accent); }
tbody tr.clickable:hover { background: var(--accent-soft); }

/* ---------- item detail ---------- */

.detail-head .pill { margin-right: 6px; }
.detail-head h1 { margin-top: 10px; }

.detail {
  display: grid;
  grid-template-columns: minmax(0, 1.4fr) minmax(290px, 1fr);
  gap: 30px;
  align-items: start;
}

.figure {
  background: #17150f;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow);
  margin-bottom: 16px;
}
.figure img {
  width: 100%;
  max-height: 68vh;
  object-fit: contain;
  display: block;
}

/* The task page's own photo band, not the general .figure cap: 68vh is right
   where a photograph carries the whole page (an asset, a reference state),
   and wrong where the ask was the opposite — a task page where the photo is
   one of several things above the fold, not the reason the fold exists. A
   fixed cap rather than a smaller vh: the point is a predictable height
   regardless of the screen, and the full size is one click away in the
   lightbox regardless of how small this is. */
.gallery-compact .figure img { max-height: 320px; }
/* A photograph we cannot produce — the volume is missing it, or a legacy
   capture has not been backfilled out of Slack yet. Say so plainly. */
.figure.broken img { display: none; }
.figure.broken::before {
  content: "Photograph unavailable — it may not have been copied from Slack yet.";
  display: block;
  padding: 46px 24px;
  text-align: center;
  color: rgba(250, 248, 245, 0.66);
  font-size: 0.86rem;
}
.figure.broken figcaption { display: none; }

.figure figcaption {
  padding: 8px 13px;
  font-size: 0.76rem;
  color: rgba(250, 248, 245, 0.6);
  text-align: right;
}
.figure:hover figcaption { color: rgba(250, 248, 245, 0.95); }

.lede {
  font-family: var(--serif);
  font-size: 1.05rem;
  line-height: 1.5;
  color: var(--ink-soft);
  margin-bottom: 22px;
}

.facts {
  background: var(--paper-raised);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  overflow: hidden;
}
.fact {
  display: grid;
  grid-template-columns: 40% 60%;
  gap: 12px;
  padding: 11px 16px;
  border-bottom: 1px solid var(--line-soft);
  font-size: 0.88rem;
}
.fact:last-child { border-bottom: none; }
.fact dt {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--ink-faint);
  padding-top: 2px;
}
.fact dd.mono { font-family: var(--mono); font-size: 0.82rem; }
.link { color: var(--accent); }
.link:hover { text-decoration: underline; }

.provenance {
  margin-top: 22px;
  padding: 15px 17px;
  background: var(--line-soft);
  border-radius: var(--radius);
}
.provenance h3 {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--ink-faint);
  font-weight: 500;
  margin-bottom: 6px;
}
.provenance p { font-size: 0.84rem; color: var(--ink-soft); }
.provenance code {
  font-family: var(--mono);
  font-size: 0.79rem;
  background: var(--paper-raised);
  padding: 1px 5px;
  border-radius: 4px;
}

@media (max-width: 860px) {
  .detail { grid-template-columns: 1fr; gap: 22px; }
  .figure img { max-height: 52vh; }
}

/* ---------- empty ---------- */

.empty {
  text-align: center;
  padding: 52px 24px;
  color: var(--ink-faint);
  background: var(--paper-raised);
  border: 1px dashed var(--line);
  border-radius: var(--radius);
}
.empty .big { font-family: var(--serif); font-size: 1.1rem; color: var(--ink-soft); margin-bottom: 5px; }
.empty p { font-size: 0.87rem; }
.empty code {
  font-family: var(--mono);
  font-size: 0.83rem;
  background: var(--line-soft);
  padding: 1px 5px;
  border-radius: 4px;
  color: var(--ink-soft);
}

/* ---------- sign in ---------- */

.signin {
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}
.signin-card {
  background: var(--paper-raised);
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow: 0 2px 6px rgba(28,26,23,.05), 0 16px 48px rgba(28,26,23,.07);
  padding: 46px 40px;
  max-width: 400px;
  width: 100%;
  text-align: center;
}
.signin-card .mark {
  font-family: var(--serif);
  font-size: 1.55rem;
  margin-bottom: 6px;
}
.signin-card .mark span { color: var(--ink-faint); font-style: italic; }
.signin-card .tag { color: var(--ink-faint); font-size: 0.88rem; margin-bottom: 30px; }

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  width: 100%;
  padding: 11px 18px;
  background: var(--ink);
  color: var(--paper-raised);
  border-radius: 8px;
  font-size: 0.92rem;
  font-weight: 500;
  transition: opacity 0.14s;
}
.btn:hover { opacity: 0.87; }

.note {
  margin-top: 22px;
  font-size: 0.78rem;
  color: var(--ink-faint);
  line-height: 1.5;
}
.error {
  background: var(--warn-soft);
  color: var(--warn);
  border-radius: 7px;
  padding: 11px 14px;
  font-size: 0.85rem;
  margin-bottom: 20px;
  text-align: left;
}

/* ---------- vendors, forms and tags ---------- */
/* The first writable surface. Forms are laid out as rows of fields that wrap,
   rather than a column, because a vendor record is mostly short values and a
   single column of thirty inputs reads as a chore. */

.page-head.with-action {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 20px;
}

.filters {
  display: flex;
  gap: 12px;
  align-items: flex-end;
  flex-wrap: wrap;
  background: var(--paper-raised);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 14px 16px;
  margin-bottom: 20px;
  box-shadow: var(--shadow);
}
.filters .f { display: flex; flex-direction: column; gap: 5px; min-width: 150px; }
.filters .f.grow { flex: 1; min-width: 240px; }

.clearlink { font-size: 0.83rem; color: var(--ink-faint); padding-bottom: 9px; }
.clearlink:hover { color: var(--accent); }

.btn.ghost {
  background: transparent;
  color: var(--ink-soft);
  border: 1px solid var(--line);
}
.btn.ghost:hover { border-color: var(--accent); color: var(--accent); opacity: 1; }

.stack { display: flex; flex-direction: column; gap: 22px; }

.fieldset {
  background: var(--paper-raised);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 20px;
  box-shadow: var(--shadow);
}
.fieldset h2 {
  font-family: var(--serif);
  font-size: 1.02rem;
  font-weight: 400;
  margin-bottom: 14px;
  padding-bottom: 9px;
  border-bottom: 1px solid var(--line-soft);
}

.rowform.boxed {
  background: var(--paper-raised);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 16px;
  margin-top: 14px;
  box-shadow: var(--shadow);
}

.check {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 0.86rem;
  color: var(--ink-soft);
  padding-bottom: 9px;
  white-space: nowrap;
}
.check input { width: auto; }

textarea {
  font-family: inherit;
  font-size: 0.9rem;
  color: var(--ink);
  background: var(--paper-raised);
  border: 1px solid var(--line);
  border-radius: 7px;
  padding: 9px 11px;
  width: 100%;
  resize: vertical;
}
textarea:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }

.formactions { display: flex; align-items: center; gap: 16px; }

/* A destructive action inside a table row should read as a link, not as a
   button competing with the row's own link. */
.linkbtn {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  font-size: 0.8rem;
  color: var(--ink-faint);
  cursor: pointer;
}
.linkbtn:hover { color: var(--warn); text-decoration: underline; }

ul.plain { list-style: none; }
ul.plain li { font-size: 0.87rem; padding: 4px 0; border-bottom: 1px solid var(--line-soft); }
ul.plain li:last-child { border-bottom: none; }
ul.plain .dim { margin-left: 6px; }

.tagrow { display: flex; flex-wrap: wrap; gap: 7px; align-items: center; }

.tagchip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: var(--accent-soft);
  color: var(--accent);
  border-radius: 20px;
  padding: 3px 5px 3px 11px;
  font-size: 0.79rem;
  font-weight: 500;
}
.tagchip a { color: inherit; }
.tagchip a:hover { text-decoration: underline; }
.tagchip form { display: inline-flex; }

.tagx {
  background: none;
  border: none;
  cursor: pointer;
  color: var(--accent);
  opacity: 0.5;
  font-size: 0.95rem;
  line-height: 1;
  padding: 0 5px;
}
.tagx:hover { opacity: 1; }

.tagadd { display: flex; gap: 8px; margin-top: 12px; }
.tagadd input { font-size: 0.85rem; padding: 6px 10px; }
.tagadd .btn { width: auto; padding: 6px 14px; font-size: 0.83rem; }

@media (max-width: 720px) {
  .page-head.with-action { flex-direction: column; }
  .filters .f, .filters .f.grow { min-width: 100%; }
}

/* ---------- footer ---------- */

.foot {
  border-top: 1px solid var(--line);
  padding: 20px 24px;
  text-align: center;
  font-size: 0.76rem;
  color: var(--ink-faint);
}

/* ---------- responsive ---------- */

@media (max-width: 720px) {
  .masthead-inner { height: auto; padding: 12px 16px; flex-wrap: wrap; gap: 12px; }
  .nav { order: 3; width: 100%; }
  .wrap { padding: 26px 16px 56px; }
  .page-head h1 { font-size: 1.6rem; }
  .who-name { display: none; }
  /* The destack: on a phone a table becomes a stack of lines. Right for the
     name-first LIST tables it was written for; catastrophic for a MATRIX,
     whose columns are its meaning — the schedule board lost its day names,
     the admin role grid became eleven bare bullets, the house month became
     35 unlabeled blocks (the mobile plan's F3). `table.keep` is the escape:
     a matrix keeps its grid and its wrapper scrolls sideways instead.
     Phase 0 deliberately keeps the destack as the DEFAULT — flipping it is
     a ~136-template blast radius that waits for each family's real
     treatment in Phase 3. */
  table:not(.keep) thead { display: none; }
  table:not(.keep) tbody td { display: block; padding: 5px 14px; }
  table:not(.keep) tbody tr { display: block; padding: 10px 0; border-bottom: 1px solid var(--line-soft); }
  table:not(.keep) tbody td:first-child { font-weight: 500; padding-top: 10px; }
}

/* ---------- admin ---------- */

.banner {
  border-radius: 8px;
  padding: 11px 15px;
  font-size: 0.87rem;
  margin-bottom: 22px;
}
.banner.good { background: var(--good-soft); color: var(--good); }
.banner.warn { background: var(--warn-soft); color: var(--warn); }

.rowform { display: flex; gap: 14px; align-items: flex-end; flex-wrap: wrap; }
.rowform .f { display: flex; flex-direction: column; gap: 5px; min-width: 150px; }
.rowform .f.grow { flex: 1; min-width: 220px; }

.rowform label,
.f label {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--ink-faint);
}
.f label .opt { text-transform: none; letter-spacing: 0; opacity: 0.7; }

input, select {
  font-family: inherit;
  font-size: 0.9rem;
  color: var(--ink);
  background: var(--paper-raised);
  border: 1px solid var(--line);
  border-radius: 7px;
  padding: 8px 11px;
  width: 100%;
}
input:focus, select:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

.btn.compact { width: auto; padding: 9px 20px; font-size: 0.87rem; }
/* A dozen templates across streams write `btn small` for a row-sized verb —
   home's queue, arrivals, menus, the inventory review queue — and no rule
   ever defined it, so every one of them got .btn's width:100% and rendered
   as a full-width slab (the owner's 31 Aug screenshot: "Confirm" one letter
   per line in a 44px grid track). Defined as what its authors meant. */
.btn.small { width: auto; padding: 4px 12px; font-size: 0.8rem; border-radius: 6px; }

.hint { font-size: 0.79rem; color: var(--ink-faint); margin-top: 5px; }

form.inline { margin: 0; }
form.inline select { font-size: 0.82rem; padding: 5px 9px; min-width: 165px; }

.tick { color: var(--accent); font-size: 0.7rem; }
.tick.off { color: var(--line); }

/* ---------- staff, rota and plans ---------- */
/* The rota is the one page here that is genuinely a grid. Everything else
   reuses the shapes the rest of the application already uses, because a staff
   page that looks like a different product is one people trust less. */

.daynav { display: flex; gap: 8px; align-items: center; }
.daynav .btn { width: auto; }

tr.past td { opacity: 0.55; }
tr.done td { opacity: 0.5; }

/* The directory's listrow columns (Phase 2 — the 5-column table's successor).
   All minmax(0, …), law 3's rider: a bare 1fr's min-content floor is the
   longest word, and Reach holds email addresses. */
.lr-staff {
  --lr-cols: minmax(0, 1.7fr) minmax(0, 1fr) minmax(0, 1.1fr)
             minmax(0, 1.4fr) minmax(0, 1.1fr);
}
/* tr.past above dims by the td; the listrow has no tds to catch. */
.lr-staff .listrow.past { opacity: 0.55; }
@media (max-width: 720px) {
  /* The second lines (reports-to, employment type, jurisdiction) are desk
     reading — the kept phone line stays `role · property`. Hiding the <br>
     removes the break the sub-line needed on the desktop grid. */
  .lr-staff .lr-sub, .lr-staff .listrow br { display: none; }
}

.card.uncovered { border-color: var(--warn); background: var(--warn-soft); }
.card.uncovered h3 a { color: var(--warn); }

.progress {
  height: 5px;
  background: var(--line-soft);
  border-radius: 3px;
  overflow: hidden;
  margin-top: 10px;
}
.progress span { display: block; height: 100%; background: var(--accent); }

.daypick { display: flex; gap: 10px; flex-wrap: wrap; padding-top: 4px; }
.daypick label {
  display: flex;
  align-items: center;
  gap: 4px;
  font-size: 0.83rem;
  text-transform: none;
  letter-spacing: 0;
  color: var(--ink-soft);
}
.daypick input { width: auto; }

.rota-wrap { overflow-x: auto; }
table.rota { font-size: 0.8rem; }
table.rota th, table.rota td { padding: 7px 9px; text-align: center; }
table.rota th.weekend { background: var(--line-soft); }
table.rota .who-cell { text-align: left; white-space: nowrap; font-size: 0.86rem; }
.rota-cell { min-width: 74px; vertical-align: middle; }
.rota-cell.shift { background: var(--accent-soft); }
.rota-cell.off { background: var(--warn-soft); color: var(--warn); }
.rota-label { font-weight: 500; white-space: nowrap; }

.tick-cell { width: 42px; }
.tickbtn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: var(--paper-raised);
  color: transparent;
  font-size: 0.7rem;
  cursor: pointer;
}
.tickbtn:hover { border-color: var(--accent); color: var(--line); }
.tickbtn.on { background: var(--good); border-color: var(--good); color: var(--paper-raised); }

.linkbtn.approve { color: var(--good); }
.linkbtn.approve:hover { color: var(--good); }

/* The rule an item comes from, kept next to the item rather than in a footnote:
   removing a statutory step should be a decision, not an accident. */
.statutory-note {
  display: block;
  margin-top: 4px;
  padding-left: 9px;
  border-left: 2px solid var(--warn-soft);
  font-style: italic;
  max-width: 52ch;
}

details.inline-edit summary { cursor: pointer; list-style: none; }
details.inline-edit summary::-webkit-details-marker { display: none; }
details.inline-edit[open] { display: block; padding-top: 8px; }
details.inline-edit .rowform { margin-top: 8px; }

.who-name.active { color: var(--accent); }

/* ---------------------------------------------------------------------------
   The mark, and the offer to install
   --------------------------------------------------------------------------- */
.wordmark { display: inline-flex; align-items: center; gap: 9px; }
.wordmark-mark {
  width: 21px;
  height: 21px;
  flex: none;
  color: var(--ink);
  /* The arch sits on a baseline of its own; nudging it down puts its foot on
     the text baseline rather than its bounding box. */
  transform: translateY(1px);
}

/* Offered once, from the corner, and never over the content it interrupts:
   full width at the bottom on a phone, a card on a desktop. */
.installbar {
  position: fixed;
  z-index: 40;
  left: 16px;
  right: 16px;
  bottom: calc(16px + env(safe-area-inset-bottom));
  display: flex;
  align-items: center;
  gap: 13px;
  padding: 13px 14px;
  background: var(--paper-raised);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: 0 2px 6px rgba(28, 26, 23, 0.06), 0 12px 32px rgba(28, 26, 23, 0.12);
  opacity: 0;
  transform: translateY(10px);
  transition: opacity 0.28s ease, transform 0.28s ease;
}
.installbar.is-in { opacity: 1; transform: none; }
.installbar[hidden] { display: none; }

@media (min-width: 640px) {
  .installbar { left: 22px; right: auto; bottom: 22px; max-width: 30rem; }
}

.installbar-mark { width: 30px; height: 30px; flex: none; color: var(--ink); }

.installbar-copy { display: grid; gap: 2px; line-height: 1.35; min-width: 0; }
.installbar-copy strong { font-family: var(--serif); font-size: 0.97rem; font-weight: 600; }
.installbar-copy span { font-size: 0.84rem; color: var(--ink-soft); }

.installbar-actions { display: flex; align-items: center; gap: 4px; margin-left: auto; }
.installbar .btn { white-space: nowrap; }

.installbar-close {
  border: 0;
  background: none;
  cursor: pointer;
  color: var(--ink-faint);
  font-size: 1.3rem;
  line-height: 1;
  padding: 2px 6px;
  border-radius: 6px;
}
.installbar-close:hover { color: var(--ink); background: var(--line-soft); }

/* Installed, and running in its own window: the masthead is the only chrome
   there is, so give it back the space the browser's toolbar used to occupy. */
@media (display-mode: standalone) {
  .masthead-inner { padding-top: calc(env(safe-area-inset-top) + 4px); }
}


/* "Or just say it" — the alternative to typing a task, on the form somebody
   opened because they did not know there was one. */
.speak-instead {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-bottom: 18px;
  padding: 14px 16px;
}
.speak-instead .speak-mic {
  width: 26px; height: 26px; flex: none;
  fill: none; stroke: var(--ink); stroke-width: 7;
  stroke-linecap: round; stroke-linejoin: round;
}
.speak-instead div { display: grid; gap: 2px; line-height: 1.35; min-width: 0; }
.speak-instead strong { font-family: var(--serif); font-size: 0.97rem; font-weight: 600; }
.speak-instead span { font-size: 0.86rem; color: var(--ink-soft); }
.speak-instead .btn { margin-left: auto; white-space: nowrap; }

@media (max-width: 560px) {
  .speak-instead { flex-wrap: wrap; }
  .speak-instead .btn { margin-left: 0; width: 100%; text-align: center; }
}


/* A room's own photograph: wide, because a room is wider than it is tall and
   a portrait crop of one tells you nothing about the space. */
.room-hero {
  margin: 0 0 18px;
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--line-soft);
  aspect-ratio: 16 / 9;
}
.room-hero img { width: 100%; height: 100%; object-fit: cover; display: block; }
.room-hero.broken { display: none; }

.room-description { max-width: 60ch; margin-bottom: 18px; }

.room-photo-form { display: flex; align-items: center; gap: 12px; margin-bottom: 22px; }
.room-photo-form label { cursor: pointer; }
.room-photo-form .dim { font-size: 0.84rem; }


/* A room card leads with its photograph where there is one. Wide, because a
   room is wider than it is tall. */
.roomcard { overflow: hidden; padding-top: 0; }
.roomcard-photo {
  display: block;
  margin: -1px -1px 12px;
  aspect-ratio: 16 / 9;
  background: var(--line-soft);
  overflow: hidden;
}
.roomcard-photo img { width: 100%; height: 100%; object-fit: cover; display: block; }


/* Floors, once a property has enough rooms that a flat grid is a wall. */
.floor-head {
  display: flex; align-items: baseline; gap: 10px;
  margin: 22px 0 10px; font-family: var(--serif); font-size: 1rem;
  color: var(--ink-soft); font-weight: 600;
}
.floor-head:first-of-type { margin-top: 4px; }
.floor-head span { font-family: var(--sans); font-size: 0.8rem; color: var(--ink-faint); font-weight: 400; }

/* Project progress: a count, shown as one. Named apart from .progress, which
   is already a bare bar on the plan pages — sharing the class gave this one
   that rule's 5px height and squashed the count beside it into a stripe. */
.progress-inline { display: inline-flex; align-items: center; gap: 8px; }
.progress-bar {
  display: block; width: 90px; height: 6px; border-radius: 3px;
  background: var(--line); overflow: hidden;
}
.progress-bar span { display: block; height: 100%; background: var(--good); }

/* The projects listrow columns (Phase 2 — the 7-column table's successor).
   Status rides .lr-side, the grid's last track, so the phone grid hands it
   the right-hand `auto` column unchanged. All minmax(0, …), law 3's rider. */
.lr-projects {
  --lr-cols: minmax(0, 2.1fr) minmax(0, 1fr) minmax(0, 1.2fr) minmax(0, 1fr)
             minmax(0, 1.1fr) minmax(0, 0.9fr) minmax(0, 0.8fr);
}
@media (max-width: 720px) {
  /* The kept phone line is `3/8 · target date`; the bar and the description
     are desk reading, and the detail page keeps both. Hiding the <br>
     removes the break the description needed on the desktop grid. */
  .lr-projects .progress-bar, .lr-projects .lr-sub,
  .lr-projects .listrow br { display: none; }
}

/* A project on a property page: name, where it stands, how far along. */
.projectcard { display: block; text-decoration: none; }
.projectcard-name { font-weight: 600; margin-bottom: 2px; }
.projectcard .meta { display: block; margin-bottom: 6px; }

/* Two actions in a page head sit side by side rather than stacking full-width,
   which is what a bare pair of .btn children does inside a flex column. */
.head-actions { display: flex; gap: 8px; flex-shrink: 0; }

/* The reorder arrows and Remove on a template row: side by side, not stacked. */
.rowactions { display: flex; align-items: center; gap: 10px; white-space: nowrap; }
.rowactions .linkbtn[disabled] { opacity: 0.25; cursor: default; }

/* Who is at this house right now. Sits above the counts because it is the
   first question anybody standing in the building asks. */
.onduty { margin-bottom: 16px; padding: 14px 16px; }
.onduty-head {
  display: flex; align-items: center; gap: 8px;
  font-size: 0.82rem; text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--ink-faint); margin-bottom: 8px;
}
.onduty .dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--line); flex-shrink: 0;
}
.onduty .dot.live { background: var(--good); }
.onduty-people { display: flex; flex-wrap: wrap; gap: 6px 20px; }
.onduty-person { font-size: 0.95rem; }
.onduty-later {
  margin-top: 10px; padding-top: 10px;
  border-top: 1px solid var(--line-soft);
  font-size: 0.88rem; color: var(--ink-soft);
}
a.card.stat { text-decoration: none; display: block; }
a.card.stat:hover { border-color: var(--accent); }

/* Attaching photographs: two inputs, because "take a photo" and "choose
   photos" are different intentions, and one running count so somebody who does
   both is told how many they are sending. */
.photopick { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.photopick .btn { cursor: pointer; }

/* A focus image with thumbnails beneath it. Several photographs of one thing
   are views of it — the front, the label, the damage — so one leads and the
   rest are a strip rather than a stack you scroll past.

   Coordination note (app.css is house ops' file and the office renders
   against it, per CLAUDE.md): this block gains a viewer and an in-place
   remove control. Nothing here touches identity — colour, type or the
   masthead — so office.css needs no change, but the components themselves
   are new and are named here for that reason. */
.gallery-focus { position: relative; margin-bottom: 10px; }
.gallery-focus figcaption { display: flex; justify-content: space-between; gap: 12px; }
.gallery-strip {
  display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 18px;
}
.gallery-tile { position: relative; width: 68px; height: 68px; flex-shrink: 0; }
.gallery-thumb {
  display: block; width: 100%; height: 100%; border-radius: 6px;
  overflow: hidden; border: 2px solid transparent;
  background: var(--line-soft);
}
.gallery-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.gallery-thumb.current { border-color: var(--accent); }
.gallery-thumb:hover { border-color: var(--ink-faint); }

/* Add Photos: one button rather than the take/choose pair the edit forms use
   — see _gallery.html for why one input is enough here. */
.gallery-addphotos { margin-bottom: 14px; }

/* The remove control on a gallery being looked at rather than edited. Reuses
   .photo-x's circle-and-confirm rather than inventing a second one; what
   differs is that this one hides until asked for, since a page for viewing
   should not open with an X drawn on every photograph.

   Revealed on hover and on keyboard focus — but not only on those: the
   .photo-row comment already found that half of this application is used on
   a phone, where there is no hover to reveal anything, so a device that
   reports it has none is shown the control at rest, the same conclusion
   reached there for the same reason. */
.gallery-remove { position: absolute; top: 6px; right: 6px; opacity: 0; transition: opacity 0.12s ease; }
.gallery-focus:hover .gallery-remove,
.gallery-tile:hover .gallery-remove,
.gallery-remove:focus-within {
  opacity: 1;
}
@media (hover: none) {
  .gallery-remove { opacity: 1; }
}
/* Higher specificity than .photo-x's own position rule (defined below, for
   the edit-form tile) rather than a same-weight override, which would lose a
   tie decided by source order rather than by which rule is meant to win. */
.gallery-remove .gallery-x { position: static; top: auto; right: auto; }

/* The photograph at full viewport. A fixed overlay rather than a new tab: the
   original already opens in one when JavaScript is off, and this is the
   upgrade for when it is on. */
.lightbox {
  position: fixed; inset: 0; z-index: 60;
  background: rgba(23, 21, 15, 0.92);
  display: flex; align-items: center; justify-content: center;
  padding: 24px;
}
.lightbox[hidden] { display: none; }
.lightbox img {
  max-width: 100%; max-height: 100%; object-fit: contain;
  box-shadow: 0 8px 40px rgba(0, 0, 0, 0.5);
}
.lightbox-close {
  position: absolute; top: 16px; right: 16px;
  width: 36px; height: 36px; border-radius: 50%;
  border: 1px solid rgba(250, 248, 245, 0.3);
  background: rgba(23, 21, 15, 0.6); color: #faf8f5;
  font-size: 1rem; line-height: 1; cursor: pointer;
}
.lightbox-close:hover { background: rgba(23, 21, 15, 0.85); }

/* Photographs at the top of the edit form: the picture is how somebody knows
   they have the right thing, so it leads rather than sitting under four
   fieldsets. The controls are visible rather than revealed on hover — half of
   this is used on a phone, where there is no hover. */
.photo-row {
  display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-start;
  margin-bottom: 18px;
}
.photo-edit { position: relative; width: 72px; }
.photo-edit img {
  width: 72px; height: 72px; object-fit: cover; display: block;
  border-radius: 6px; border: 2px solid transparent; background: var(--line-soft);
}
.photo-edit.is-lead img { border-color: var(--accent); }
.photo-x {
  position: absolute; top: -6px; right: -6px;
  width: 20px; height: 20px; line-height: 1; padding: 0;
  border-radius: 50%; border: 1px solid var(--line);
  background: var(--paper-raised); color: var(--ink-soft);
  font-size: 0.66rem; cursor: pointer;
}
.photo-x:hover { color: var(--warn); border-color: var(--warn); }
.photo-tag {
  display: block; margin-top: 4px; text-align: center;
  font-size: 0.68rem; color: var(--ink-faint); letter-spacing: 0.02em;
}
.photo-tag.as-button {
  background: none; border: none; cursor: pointer; width: 100%;
  font-family: inherit;
}
.photo-tag.as-button:hover { color: var(--accent); text-decoration: underline; }

/* The add control, sized to sit in the same row as the thumbnails. */
.photo-add {
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  width: 72px;
}
/* Classes rather than `label[for]`: an attribute selector outranks a bare
   class, so styling the tile that way made every label in here 1.4rem — the
   word "Choose" came out the size of a heading and broke the row. */
.photo-add .plus {
  width: 72px; height: 72px; border-radius: 6px;
  border: 1px dashed var(--line); background: var(--paper-raised);
  display: flex; align-items: center; justify-content: center;
  font-size: 1.4rem; line-height: 1; color: var(--ink-faint); cursor: pointer;
}
.photo-add .plus:hover { border-color: var(--accent); color: var(--accent); }
.photo-add .pick {
  font-size: 0.68rem; color: var(--ink-faint); cursor: pointer;
  text-align: center; width: 100%;
}
.photo-add .pick:hover { color: var(--accent); text-decoration: underline; }
.photo-count { align-self: center; font-size: 0.8rem; }

/* Leaving the register: collapsed, so the ordinary business of the page is the
   whole page until somebody is actually getting rid of something. */
.leaving { margin-top: 26px; }
.leaving > summary {
  cursor: pointer; font-size: 0.88rem; color: var(--ink-soft);
  padding: 8px 0; border-top: 1px solid var(--line-soft);
}
.leaving > summary:hover { color: var(--accent); }
.leaving[open] > summary { margin-bottom: 14px; }
.leaving h3 { font-family: var(--serif); font-weight: 400; margin-bottom: 10px; }
.leaving .warn-card { border-color: var(--warn); margin-bottom: 14px; }
.leaving .warn-card ul { margin: 8px 0 0 18px; font-size: 0.9rem; }

/* A fact worth acting on rather than a failure: a donation closed without its
   receipt is fine today and a problem in April. */
.hint.warnish { color: var(--warn); }

/* A fact worth acting on rather than a failure: a donation closed without its
   receipt is fine today and a problem in April. */
.hint.warnish { color: var(--warn); }

/* Where to look, before what to look for. A register heading for thousands of
   rows needs a first question, and the first question is always "where". */
.shelf { display: block; text-decoration: none; }
.shelf-count { font-family: var(--serif); font-size: 1.7rem; line-height: 1.1; }
.shelf-label { font-weight: 600; margin-top: 2px; }
.shelf:hover { border-color: var(--accent); }

/* Paperwork attached to a thing: a manual, a warranty, a receipt. */
.doclist li { display: flex; align-items: center; gap: 10px; padding: 5px 0; }
.doclist .pill { flex-shrink: 0; }

/* A labelled group inside a dropdown. Inventory is a system rather than a
   page — a register, what is running out, where it is kept — and the label is
   what makes those read as one area rather than four unrelated links. */
.navlabel {
  padding: 6px 14px 2px;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-faint);
}

/* Links found on the web and not yet vouched for by anybody. Set apart from
   attached paperwork on purpose — one is a decision, the other is a
   suggestion. */
.candidates { margin: 12px 0; border-style: dashed; }

/* Your day. The current state is the whole screen, because somebody standing
   in a doorway with a bag in one hand should not have to read. */
.daycard { text-align: center; padding: 28px 20px; }
.daycard.on { border-color: var(--good); }
.daystate {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--ink-faint);
}
.dayclock {
  font-family: var(--serif); font-size: 3.4rem; line-height: 1.05;
  margin: 6px 0 2px;
}
.dayclock span { font-size: 1.4rem; color: var(--ink-faint); }
.dayform { margin-top: 18px; display: grid; gap: 12px; text-align: left; }
.btn.big {
  width: 100%; padding: 16px; font-size: 1.05rem; border-radius: 10px;
}
.warn-text { color: var(--warn); }

/* An entry that has been open long enough to be a forgotten Finish. */
tr.flagged { background: var(--warn-soft); }

/* ---------------------------------------------------------------------------
   Dictation
   ---------------------------------------------------------------------------
   The button is injected by /static/js/dictate.js and exists only where speech
   recognition does, so there is no disabled state to style — a microphone that
   cannot listen is never drawn. */
.dictate-bar { display: flex; align-items: center; gap: 10px; margin-top: 6px; }
.dictate {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 6px 12px; border: 1px solid var(--line); border-radius: 999px;
  background: var(--paper-raised); color: var(--ink-soft);
  font-size: 0.78rem; cursor: pointer;
}
.dictate:hover { border-color: var(--ink-faint); }
.dictate svg {
  width: 13px; height: 13px;
  stroke: currentColor; fill: none; stroke-width: 9;
  stroke-linecap: round; stroke-linejoin: round;
}
.dictate.listening { background: var(--ink); border-color: var(--ink); color: var(--paper); }
/* Ending a turn is a deliberate act (8f77c6c4): while listening the button
   grows into the row's loudest control — bigger text, a halo, thumb-sized —
   so "Done" is the interface and the silence timer is only the fallback. */
.dictate.listening {
  padding: 10px 18px; font-size: 0.95rem; min-height: 44px;
  box-shadow: 0 0 0 4px var(--indigo-soft, rgba(63, 81, 181, 0.18));
}
.dictate.listening .dictate-label { font-weight: 700; }
/* Breathing rather than blinking: a pulse that never fully disappears reads as
   "still going" where a blink reads as an error. */
.dictate.listening svg { animation: dictate-breathe 1.6s ease-in-out infinite; }
@keyframes dictate-breathe { 0%, 100% { opacity: 1; } 50% { opacity: 0.45; } }
/* CONNECTING is deliberately the opposite of listening: hollow where
   listening is solid, a dashed edge where listening is filled — the owner
   read the old single look as "already recording" and lost the first words
   of a turn into the handshake. Speak after the tone; this look says the
   tone hasn't sounded yet. */
.dictate.connecting {
  border-style: dashed; border-color: var(--ink-faint);
  background: var(--paper-raised); color: var(--ink-faint);
}
.dictate.connecting svg { animation: dictate-breathe 0.8s ease-in-out infinite; }
.dictate-status { font-size: 0.75rem; color: var(--ink-faint); }

@media (prefers-reduced-motion: reduce) {
  .dictate.listening svg, .dictate.connecting svg { animation: none; }
}

/* ---------------------------------------------------------------------------
   The reference-state photo editor
   ---------------------------------------------------------------------------
   One card per photograph: the picture, and beside it the note that says what
   the picture cannot. Stacked on a phone, because that is where these are
   written — standing at the drawer with it open. */
.refphotos { display: grid; gap: 14px; }
.refphoto {
  display: grid; grid-template-columns: 200px 1fr; gap: 16px;
  padding: 14px; border: 1px solid var(--line); border-radius: 10px;
  background: var(--paper-raised);
}
.refphoto.leads { border-color: var(--ink-faint); }
.refphoto-img {
  position: relative; display: block; border-radius: 8px; overflow: hidden;
  background: var(--line-soft); aspect-ratio: 4 / 3;
}
.refphoto-img img { width: 100%; height: 100%; object-fit: cover; display: block; }
.refphoto-note { display: grid; gap: 8px; align-content: start; }
.refphoto-note textarea { width: 100%; }
.refphoto-actions {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  font-size: 0.78rem;
}
.refphoto-seq {
  font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--ink-faint);
}

/* The read view: a photograph and its note, together. A note detached from
   its picture is an instruction about nothing. */
.refgallery { display: grid; gap: 20px; }
.refgallery figure {
  border: 1px solid var(--line); border-radius: 10px; overflow: hidden;
  background: var(--paper-raised);
}
.refgallery img { width: 100%; display: block; }
.refgallery figcaption { padding: 12px 14px; font-size: 0.88rem; }
.refgallery .analysis {
  margin-top: 6px; font-size: 0.78rem; color: var(--ink-faint);
}

@media (max-width: 640px) {
  .refphoto { grid-template-columns: 1fr; }
  .refphoto-img { aspect-ratio: 3 / 2; }
}

/* Present for a screen reader, absent for everyone else. The photo-note fields
   are labelled by the picture beside them, which a sighted person reads and a
   screen reader cannot. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

/* A photograph that fails to load says so, rather than showing the browser's
   broken-image glyph and looking like a bug in the page. */
.refphoto-img.broken img { display: none; }
.refphoto-img.broken::after {
  content: "Photograph unavailable";
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  font-size: 0.75rem; color: var(--ink-faint); text-align: center; padding: 8px;
}

/* The per-state edit links under a room's reference-state cards. A card is a
   link to the state, so the edit cannot live inside it — a link inside a link
   is not markup a browser will honour. */
.refstate-edits { display: flex; gap: 16px; flex-wrap: wrap; margin-top: 12px; }

/* A note is somebody talking. The body is the point of the card, so it is set
   as prose rather than as a metadata line with a sentence attached.
   Still used by task.html, trip_ideas.html and trip_research.html — the notes
   FEED moved to the row layout below, these did not. */
.notecard { padding: 14px 16px; }
.notecard-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; margin-bottom: 8px; flex-wrap: wrap;
}
.notecard-head form { margin: 0; }
.notebody {
  font-size: 0.98rem; line-height: 1.55; color: var(--ink);
  white-space: pre-wrap;
}

/* The notes feed, as an inbox.
   This was one bordered card per note with four stacked blocks inside it, so
   six notes filled a phone and the only way to read the page was to scroll
   it. A feed whose purpose is scanning what has been said should fit more
   than six things. Rows now share one frame and one rule between them, which
   is what makes a list read as a list rather than as a pile of objects. */
.notelist {
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--paper-raised); overflow: hidden;
}
.noterow { padding: 10px 14px; border-bottom: 1px solid var(--line-soft); }
.noterow:last-child { border-bottom: 0; }
/* The one still-waiting marker that survives at this density. A pill saying
   "Waiting" down forty rows becomes wallpaper and eats the width the note
   itself needs; the edge does the same work in three pixels. */
.noterow.waiting { border-left: 3px solid var(--warn); padding-left: 11px; }

/* Line one: what it is about, who said it, when. Baseline-aligned so the
   three sit on one line rather than drifting against each other. */
.notemeta {
  display: flex; align-items: baseline; gap: 8px;
  font-size: 0.84rem; margin-bottom: 2px;
}
.notedot {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--warn); flex-shrink: 0; align-self: center;
}
/* The task name gets the room, and the author gives it up. Both were
   truncating at a share of the width, which on a phone left "Regrout..."
   beside a full email address — exactly backwards, since the task is what
   the row is about and the author is the least of the three things on the
   line. So the name takes what it needs and shrinks last; the author shrinks
   first and ellipses. */
.notetask {
  font-weight: 600; color: var(--ink); text-decoration: none;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  flex: 0 1 auto; min-width: 0;
}
.notetask:hover { color: var(--accent); text-decoration: underline; }
/* A note with no task behind it. Same slot, same weight, but plainly not a
   link — there is nothing to open, and a label that looks clickable and is
   not is worse than one that never pretended. */
.notetask-none { color: var(--ink-faint); font-weight: 500; }
.notewho {
  color: var(--ink-soft); white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
  flex: 1 1 auto; min-width: 0;
}
/* Pushed to the right edge, where a date is looked for rather than read. */
.notewhen { color: var(--ink-faint); margin-left: auto; white-space: nowrap; flex-shrink: 0; }

/* Line two: the note. Still the largest thing in the row — it is the reason
   the row exists — just no longer surrounded by its own box. Scoped to
   .noterow so the shared .notebody above keeps its own size on the three
   pages that still render cards. */
.noterow .notebody { font-size: 0.95rem; line-height: 1.45; }

/* Line three, and only when it says something. Small and quiet: where the
   task lives and who has it decides which note to open perhaps one time in
   ten, so it gets the space that ratio deserves. */
.notefoot {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px; margin-top: 3px; font-size: 0.78rem;
}
.notefoot form { margin: 0; }
.notedone {
  font: inherit; font-size: 0.78rem; color: var(--ink-faint);
  background: none; border: 0; padding: 0; cursor: pointer;
  text-decoration: underline; white-space: nowrap;
}
.notedone:hover { color: var(--accent); }

/* On a phone the inbox tightens (Phase 2 of the mobile plan): the body clamps
   to three lines — most notes fit whole, a long one is a tap away on the task
   the row already links to — and the foot's task-string goes with it (where
   the task lives decides which note to open one time in ten; the link answers
   the other nine). Mark read stays, at tap size: it is the row's one action. */
@media (max-width: 720px) {
  .noterow .notebody {
    display: -webkit-box; -webkit-box-orient: vertical;
    -webkit-line-clamp: 3; overflow: hidden;
  }
  .notefoot > .dim { display: none; }
  .notedone { min-height: var(--tap); display: inline-flex; align-items: center; }
}

/* The count beside a nav item. Small, and only drawn when there is something
   in it — a badge showing zero is a badge people learn to skip. */
.navcount {
  display: inline-block; min-width: 18px; padding: 0 5px;
  margin-left: 6px; border-radius: 9px;
  background: var(--accent); color: var(--paper);
  font-size: 0.7rem; line-height: 18px; text-align: center;
}

/* Work at a house, grouped by what produced it. The summary row is the whole
   control: a routine with nineteen tasks is one line until somebody wants the
   nineteen. */
.workgroup { border: 1px solid var(--line); border-radius: var(--radius); margin-bottom: 10px; background: var(--paper-raised); }
.workgroup > summary {
  cursor: pointer; padding: 12px 16px; display: flex; align-items: baseline;
  gap: 10px; flex-wrap: wrap; list-style: none;
}
.workgroup > summary::-webkit-details-marker { display: none; }
.workgroup > summary::before {
  content: "▸"; color: var(--ink-faint); font-size: 0.8rem; flex-shrink: 0;
}
.workgroup[open] > summary::before { content: "▾"; }
.workgroup > summary:hover { color: var(--accent); }
.workgroup .wg-name { font-weight: 600; }
.workgroup .wg-meta { color: var(--ink-faint); font-size: 0.86rem; }
.workgroup .wg-body { padding: 0 16px 12px 34px; }
.workgroup .wg-body li { padding: 5px 0; border-bottom: 1px solid var(--line-soft); }
.workgroup .wg-body li:last-child { border-bottom: 0; }

/* A rental says how long is left before it says anything else. */
.rentalwindow { font-size: 0.86rem; color: var(--ink-faint); }
.rentalwindow .imminent { color: var(--warn); font-weight: 600; }

/* ---------------------------------------------------------------------------
   The travel planner
   ---------------------------------------------------------------------------
   A trip is read on a phone in an airport, so the day strip scrolls sideways
   and everything below it is one column. .slot-when is a fixed column rather
   than flowing, because what makes a fortnight scannable is that Thursday
   afternoon sits under Wednesday afternoon. Built from the existing tokens
   throughout — a trip page that looked like a different product would be a
   worse page, not a nicer one. */
/* ---------------------------------------------------------------------------
   Trips
   ---------------------------------------------------------------------------
   A trip is read in two places: at a desk weeks out, deciding, and on a phone
   in an airport with one hand free. The second is the one that must not fail,
   so all of this is one column that scrolls, and the only thing that moves
   sideways is the strip of dates — where sideways is what a row of days is.

   The planner the principal sketched used tabs. It does not here: nothing else
   in this application hides a section behind a control, and every reason the
   masthead menus are also real links applies to a tab twice over — the back
   button, a reload, and "send Nina the packing list", which has to be a URL. */

/* The trip's own sub-navigation. Four sibling pages, wrapping rather than
   scrolling, because a wrapped row loses nothing and a scrolled one hides the
   fourth item on the phone this is mostly read on. */
.tripnav {
  display: flex; flex-wrap: wrap; gap: 4px;
  margin-bottom: 24px; padding-bottom: 12px;
  border-bottom: 1px solid var(--line);
}
.tripnav a {
  padding: 6px 12px; border-radius: 7px; font-size: 0.86rem;
  color: var(--ink-soft); transition: background 0.12s, color 0.12s;
}
.tripnav a:hover { background: var(--line-soft); color: var(--ink); }
.tripnav a.current { background: var(--accent-soft); color: var(--accent); font-weight: 500; }
.tripnav .dim { font-size: 0.78rem; }

/* The days, as a rail. A fortnight does not fit across a phone and never will,
   so this scrolls rather than wraps: wrapping loses the one thing the shape is
   for, which is that the days run in order and today is somewhere along them.
   Snap points so a thumb lands on a day rather than between two. */
.daystrip {
  display: flex; gap: 8px; overflow-x: auto;
  scroll-snap-type: x proximity; -webkit-overflow-scrolling: touch;
  padding: 2px 2px 12px; margin-bottom: 18px;
}
.daystrip::-webkit-scrollbar { height: 0; }
.daystrip a {
  flex: none; scroll-snap-align: start; min-width: 62px;
  padding: 9px 10px 10px; text-align: center;
  background: var(--paper-raised); border: 1px solid var(--line);
  border-radius: var(--radius);
}
.daystrip a:hover { border-color: var(--ink-faint); }
.daystrip .dow {
  display: block; font-size: 0.67rem; text-transform: uppercase;
  letter-spacing: 0.07em; color: var(--ink-faint);
}
.daystrip .dom {
  display: block; font-family: var(--serif); font-size: 1.34rem; line-height: 1.15;
}
/* A dot, not a count. At this size the only readable question is "does this day
   have anything in it"; a number nobody can read is decoration. */
.daystrip .n {
  display: block; width: 5px; height: 5px; margin: 6px auto 0;
  border-radius: 50%; background: var(--line);
}
.daystrip .n.filled { background: var(--accent); }
/* Today, when today is inside the trip. Marked here as well as on the day
   itself, because the rail is what the eye lands on first. */
.daystrip a.today { border-color: var(--accent); background: var(--accent-soft); }
.daystrip a.today .dom { color: var(--accent); }

/* The day being READ, as against today. With one day rendered at a time the
   rail is the only thing that says which, and the two are different facts:
   today is where the trip is, this is where the reader is. Solid rather than
   tinted so "here" wins when they are the same day — a reader who cannot tell
   which day is on screen has lost the only navigation there is.

   Styled off [aria-current] and not off a class, so the highlight and what a
   screen reader is told are one attribute. It is also the one thing CSS cannot
   carry across the DOM — the chip and the block it shows are in different
   subtrees — so the server marks the default day and trip.js moves the
   attribute on a tap. With the script dead the mark simply stays on the default
   while the days still switch: a stale highlight, not a broken page. */
.daystrip a[aria-current="page"] {
  background: var(--accent); border-color: var(--accent);
}
.daystrip a[aria-current="page"] .dow,
.daystrip a[aria-current="page"] .dom { color: var(--paper-raised); }
.daystrip a[aria-current="page"] .n { background: var(--paper-raised); opacity: 0.5; }
.daystrip a[aria-current="page"] .n.filled { opacity: 1; }

/* One day: a .card holding a header band and its slots, rather than five loose
   cards. The day is the unit somebody plans in, and the boundary is what stops
   a fortnight reading as seventy unrelated rows. */
.dayblock { padding: 0; overflow: hidden; margin-bottom: 14px; }
.dayblock-head {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  padding: 12px 16px; background: var(--paper);
  border-bottom: 1px solid var(--line);
}
.dayblock-head h3 { font-family: var(--serif); font-size: 1.05rem; font-weight: 400; }
.dayblock-head .meta { font-size: 0.8rem; color: var(--ink-faint); margin-left: auto; }
/* Deliberately not sticky. The masthead already is, and two sticky bars on a
   phone leave a reading window three lines tall. */
.dayblock.today { border-color: var(--accent); }
.dayblock.today .dayblock-head { background: var(--accent-soft); }

/* ---------- one day at a time ----------
   Every day is rendered and one is shown. The complaint this answers was
   VISUAL load and not bytes — a fortnight is seventy slots to wade through on a
   phone, and the page was never slow — so the fix is what is on screen, and no
   query on the trip page is narrowed to achieve it. That matters: the rail's
   own filled dots, "9 of 70 slots decided" and the packing counts are facts
   about the whole trip, and a narrowed query would make the page lie about the
   trip to save three rows in a batch it already makes.

   :target AND NOT A RADIO GROUP. Both work with no JavaScript; only one puts
   the day in the URL, which is what the back button, a reload and "send Nina
   Thursday" all need. It is the same argument .tripnav already makes out loud
   about being links rather than tabs.

   INSIDE @supports, WHICH IS THE WHOLE SAFETY OF IT. The default-day rule needs
   :has(), and a browser without it would drop that rule while keeping the
   `display: none` — so a page opened with no fragment would show NO day at all.
   Guarding the hiding as well means the fallback is that every day shows, which
   is exactly what this page did before and is not a break. */
@supports selector(:has(*)) {
  .days .dayblock { display: none; }
  .days .dayblock:target { display: block; }
  /* No fragment: show the day the server nominated — today when the trip is
     running, its first day otherwise. */
  .days:not(:has(.dayblock:target)) .dayblock.is-default { display: block; }

  /* The Today card repeats today's block, and did so deliberately: the
     alternative used to be scrolling a fortnight to find out whether dinner is
     booked. One day at a time answers that instead, so the card is now for the
     reader who has gone somewhere else — hidden when today's block is the one
     on screen, which is either because it is the target or because nothing is.
     Decided here and not in the template because which day is on screen is a
     fragment, and a fragment never reaches the server. */
  body:not(:has(.dayblock:target)) .todaycard,
  body:has(.dayblock.today:target) .todaycard { display: none; }
}

/* Where a day block lands when its chip is tapped. The masthead is sticky and
   the rail is not, so without this the jump puts the day's first slot under the
   header and the rail — the only navigation this section has — off the top of
   the screen. Enough for both: 63px of masthead and the rail beneath it. */
.dayblock { scroll-margin-top: 168px; }

/* ---------- Who is Going ----------
   Three groups off one column, each with its own heading and its own count. */
.grouphead {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  margin: 20px 0 9px; font-family: var(--serif);
  font-size: 0.98rem; font-weight: 400;
}
.grouphead .meta {
  font-family: var(--sans); font-size: 0.78rem; color: var(--ink-faint);
}

/* One tap on, one tap off. A button rather than a link because it posts, and a
   pill rather than a checkbox because "is Alex coming" is answered forty times
   a fortnight by somebody holding a phone in one hand. Lit is on the trip; the
   unlit ones are the rest of the household, in the same strip, because
   rendering "who is on" and "who could be added" as two rows means finding
   which one Alex is in before you can act on him. */
.pill.toggle {
  border: 1px solid var(--line); background: var(--paper-raised);
  color: var(--ink-soft); font: inherit; font-size: 0.79rem; font-weight: 500;
  padding: 7px 13px; cursor: pointer; text-align: left;
}
.pill.toggle .dim { color: var(--ink-faint); font-weight: 400; }
.pill.toggle:hover { border-color: var(--ink-faint); color: var(--ink); }
.pill.toggle.on {
  background: var(--accent-soft); border-color: var(--accent); color: var(--accent);
}
.pill.toggle.on .dim { color: var(--accent); opacity: 0.72; }

/* Somebody employed here, travelling to support. A rule down the side rather
   than a colour of its own: the pill's colour is already saying whether they
   are coming, and a fourth pill colour on the same row would read as a fourth
   kind of answer. */
.pill.support { box-shadow: inset 3px 0 0 var(--ink-faint); padding-left: 12px; }

/* What somebody said about a place to stay. Prose, so it gets the whole line
   and the attribution sits under it — ul.rows is a flex row, which is right for
   a link and a button and wrong for a sentence. */
.saidlist li { display: block; }
.saidlist li > span:first-child { display: block; }
.saidlist .dim { display: block; margin-top: 3px; font-size: 0.78rem; }

/* Morning, lunch, afternoon, dinner, evening. The label is a fixed column so
   the slots line up down the whole trip and the eye can run "dinner, dinner,
   dinner" without reading — which is what checking a fortnight of bookings
   actually is. On a phone the column would leave 40% of the width to the word
   "afternoon", so it stacks instead. */
.slot {
  display: grid; grid-template-columns: 96px minmax(0, 1fr);
  gap: 4px 14px; padding: 12px 16px;
  border-bottom: 1px solid var(--line-soft);
}
.slot:last-child { border-bottom: 0; }
.slot-when {
  font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.07em;
  color: var(--ink-faint); padding-top: 3px;
}
.slot-pick { min-width: 0; }
.slot-pick .name { font-weight: 600; }
.slot-pick .where { font-size: 0.84rem; color: var(--ink-soft); }
.slot-pick .entry + .entry { margin-top: 10px; }
/* Nothing chosen. Set apart rather than left blank, because an empty afternoon
   is the thing a planner exists to show you. */
.slot.open .slot-pick { color: var(--ink-faint); }
/* A club dinner, a lift window, a departure slot: fixed by somebody else, so it
   is shown rather than offered. A left rule rather than a pill — this is a
   property of the row, not a label sitting in it. */
.slot.fixed { border-left: 3px solid var(--warn); }
.slot.fixed .slot-when { color: var(--warn); }

/* Somebody who said no. Struck through rather than absent, because "Nina is not
   coming to dinner" and "nobody has asked Nina" are different facts and a
   missing chip says both. An unanswered person renders as a plain .pill. */
.pill.out { opacity: 0.5; text-decoration: line-through; }

/* Love, maybe, pass — for one person, you. Everyone else's votes are a tally in
   .pill beside it, because a vote you cannot change is a fact rather than a
   control. Three buttons rather than a select: this is tapped once per idea by
   people who will not open a dropdown forty times. */
.votebar {
  display: inline-flex; border: 1px solid var(--line);
  border-radius: 999px; overflow: hidden; flex: none;
}
.votebar button {
  border: 0; border-right: 1px solid var(--line); background: var(--paper-raised);
  font: inherit; font-size: 0.78rem; color: var(--ink-faint);
  padding: 6px 13px; cursor: pointer;
}
.votebar button:last-child { border-right: 0; }
.votebar button:hover { background: var(--line-soft); color: var(--ink); }
.votebar button.on { color: var(--paper-raised); }
.votebar button.on.love  { background: var(--good); }
.votebar button.on.maybe { background: var(--ink-faint); }
.votebar button.on.pass  { background: var(--warn); }

/* The compiled brief, as text rather than as a rendered document. What this
   produces is pasted into a message to a coordinator, and a page that looks
   like the paste is the only way to know what the paste contains. */
.briefbox {
  font-family: var(--mono); font-size: 0.82rem; line-height: 1.6;
  white-space: pre-wrap; overflow-wrap: break-word;
  background: var(--paper-raised); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow);
  padding: 18px; max-height: 62vh; overflow-y: auto;
}
.briefactions {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 12px;
}

@media (max-width: 640px) {
  .slot { grid-template-columns: 1fr; gap: 3px; padding: 11px 14px; }
  .slot-when { padding-top: 0; }
  .votebar button { padding: 7px 15px; }   /* a thumb, not a pointer */
  .briefbox { max-height: none; padding: 14px; }
}

/* Seven top-level nav items became eight with Trips. .nav has never wrapped and
   never scrolled, so an eighth simply squeezes the other seven. Wrapping is the
   whole fix — the row is full-width already. */
.nav { flex-wrap: wrap; }

/* ---------- the activity feed ----------
   Minimalist on purpose: one line per thing, and the detail only when somebody
   asks for it. A feed that shows everything at once is a wall, and a wall gets
   scrolled past rather than read. */

.feed { list-style: none; margin: 0; padding: 0; }

.feed-item {
  border-bottom: 1px solid var(--line-soft);
  padding: 2px 0;
}
.feed-item:last-child { border-bottom: 0; }

.feed-line {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 9px 10px 9px 12px;
  border-radius: 7px;
  border-left: 2px solid transparent;
}
.feed-line:hover { background: var(--line-soft); }

/* Unread is a mark, not a colour wash. Somebody with forty unread items should
   see a column of small marks rather than a page that looks broken. */
.feed-item.unseen > .feed-line { border-left-color: var(--accent); }
.feed-item.unseen .feed-title { font-weight: 600; }

.feed-kind {
  flex-shrink: 0;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--ink-faint);
  min-width: 92px;
}
.feed-title { flex: 1; font-size: 0.92rem; color: var(--ink); }
.feed-when { flex-shrink: 0; font-size: 0.78rem; color: var(--ink-faint); }

.feed-detail {
  padding: 0 12px 10px 114px;
  font-size: 0.88rem;
  color: var(--ink-soft);
}
.feed-detail p { margin: 0; }
.feed-detail .feed-meta { margin-top: 3px; font-size: 0.8rem; color: var(--ink-faint); }

/* On a device that can hover, the detail waits to be asked for. `:focus-within`
   carries the keyboard, so tabbing to the link reveals the same thing. */
@media (hover: hover) and (pointer: fine) {
  .feed-detail { display: none; }
  .feed-item:hover .feed-detail,
  .feed-item:focus-within .feed-detail { display: block; }
}

/* On touch the detail stays, CLAMPED to one line (Phase 2 of the mobile
   plan): always-expanded made every phone row ~2.5× a laptop's and the feed
   ~3,000px for thirty entries. Not hidden — a finger has no hover to ask
   with, and every row already links to the full story — so one line keeps
   the scent and the meta line keeps the who/where. */
@media (hover: none) {
  .feed-detail { padding-left: 12px; }
  .feed-detail > p:not(.feed-meta) {
    display: -webkit-box; -webkit-box-orient: vertical;
    -webkit-line-clamp: 1; overflow: hidden;
  }
}

/* Text that is only true where a pointer is. Hidden by default and shown under
   the same query as the behaviour it describes, so the two cannot drift: an
   instruction to hover must not survive on a device that cannot. */
.pointer-only { display: none; }
@media (hover: hover) and (pointer: fine) {
  .pointer-only { display: inline; }
}

/* Narrower: the kind label stops holding a column open. */
@media (max-width: 620px) {
  .feed-kind { min-width: 0; }
  .feed-line { flex-wrap: wrap; gap: 4px 8px; }
  .feed-detail { padding-left: 12px; }
}

/* ---------------------------------------------------------------------------
   A day's activities, collapsed
   ---------------------------------------------------------------------------
   Every activity used to render expanded, with its attendance form inline. A
   fortnight of that is a page nobody scrolls to the end of, and the one thing
   people actually do to an activity — say who is coming — was buried in it.

   So an activity is one tappable line, and <details> holds the rest. No
   JavaScript: <details> is the only disclosure control a browser gives keyboard
   and screen-reader behaviour to for free, and this page is read on a phone in
   an airport where a script that has not loaded is a page that does not work.

   The tap targets are sized for a thumb rather than a pointer, which is why
   .attend is padded past what it looks like it needs. */
/* An activity, collapsed
   ---------------------------------------------------------------------------
   A fortnight is forty of these and one of them is being tapped. The head is the
   whole control: a clock in a fixed column so Thursday's 7:30 sits under
   Wednesday's, then what it is, then how many are in — which is the number
   somebody opened the page for. 44px of head, because the thing tapping it is a
   thumb and not a pointer. */
details.entry { border-top: 1px solid var(--line-soft); }
.slot-pick > details.entry:first-child { border-top: 0; }
summary.entry-head {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 3px 8px;
  padding: 12px 0; min-height: 44px; cursor: pointer; list-style: none;
}
summary.entry-head::-webkit-details-marker { display: none; }
summary.entry-head::before { content: "▸"; color: var(--ink-faint); font-size: .8rem; flex: none; }
details.entry[open] > summary.entry-head::before { content: "▾"; }
summary.entry-head:hover .name { color: var(--accent); }
/* Tabular figures and a fixed width so the times form a column rather than a
   ragged edge — that column is what makes a fortnight of bookings scannable. */
.entry-head .when {
  flex: none; min-width: 5.2em; font-size: .82rem; color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}
.entry-head .name { font-weight: 600; }
.entry-head .pill { font-size: .7rem; padding: 2px 7px; }
.entry-head .whocount {
  margin-left: auto; font-size: .79rem; color: var(--ink-faint); white-space: nowrap;
}
.entry-head .whocount b { color: var(--ink); font-weight: 600; }
/* Not asked, and booked for fewer than said yes: the two facts on this page
   worth somebody's attention before they travel. */
.entry-head .whocount .ask { color: var(--warn); }
.entry-body { padding: 0 0 14px; }

/* Who is coming, one row per traveller. A grid rather than a flex row so the
   controls line up down the list however long a name is. */
.attend { display: block; margin-top: 6px; }
.attend-row {
  display: grid; grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: center; gap: 4px 10px; padding: 6px 0;
  border-bottom: 1px solid var(--line-soft);
}
.attend-row:last-child { border-bottom: 0; }
.attend-who { font-size: .9rem; }
.attend-said { font-size: .75rem; color: var(--ink-faint); }
.votebar button.in.on  { background: var(--good); border-color: var(--good); }
.votebar button.out.on { background: var(--warn); border-color: var(--warn); }

/* Tap a time. Chips rather than a wheel, and only ever writing into the native
   input beside them — see trip.js. Built by script, so these rules style
   something that is absent unless it works. */
.whenfield { display: flex; gap: 14px; align-items: flex-start; flex-wrap: wrap; }
.whenfield .f { min-width: 150px; }
.whenfield select:disabled { opacity: .55; }
.timechips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.timechips button {
  min-height: 40px; padding: 0 14px; border: 1px solid var(--line);
  border-radius: 999px; background: var(--paper-raised);
  font: inherit; font-size: .82rem; color: var(--ink-soft); cursor: pointer;
  font-variant-numeric: tabular-nums;
}
.timechips button:hover { border-color: var(--accent); color: var(--accent); }
.timechips button.on {
  background: var(--accent); border-color: var(--accent); color: var(--paper-raised);
}

/* A confirmation number is read out at a desk, so it is monospaced wherever it
   appears. There has been a bare .mono in the markup since 046 with no rule
   behind it — only td .mono and .fact dd.mono existed. */
.mono { font-family: var(--mono); font-size: .82rem; }

@media (max-width: 640px) {
  .entry-head .whocount { margin-left: 0; flex-basis: 100%; }
  .attend-row { grid-template-columns: minmax(0, 1fr) auto; padding: 8px 0; }
  .attend-said { grid-column: 1; font-size: .72rem; }
  /* Thumbs, on the ideas page's votebar as well as this one. */
  .votebar button { min-height: 44px; padding: 0 18px; }
  .timechips button { min-height: 44px; padding: 0 16px; }
}

/* Demo mode. Loud on purpose: this bar is the difference between "the
   navigation is broken" and "I am currently a nanny". Sticky so it survives
   scrolling, and above the masthead so it is never the second thing seen. */
.demo-banner {
  position: sticky;
  top: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  padding: 8px 16px;
  background: var(--warn-bg, #6b3b00);
  color: var(--warn-ink, #fff);
  font-size: 0.9rem;
}
.demo-banner form { margin: 0; }
.demo-banner .btn { background: transparent; border-color: currentColor; color: inherit; }

/* The help bubble's styles now live in static/css/help-bubble.css, loaded
   alongside this file by base.html and by the three pop-out module shells
   (art, wardrobe, jewelry) which load no other stylesheet of ours. Moved
   rather than copied: three duplicates of a component's CSS is how two of
   them drift. The one bubble rule NOT there is the dock collision fix further
   down this file, which is about the dock rather than the bubble. */

/* -----------------------------------------------------------------------
   The masthead on a touch screen
   -----------------------------------------------------------------------
   The nav above was built for a pointer: eleven entries in a row, five of
   them dropdowns that open on hover. On a phone `flex-wrap` turned that row
   into a wall of links about two thirds of a screen tall, so "what am I meant
   to be doing" began below the fold — and the five carets did nothing, since
   a tap on a group label follows its link. Storage, Groceries, Restock,
   Donations, Valuables, Routines, Notes feed and Schedule had no route on the
   device this application is mostly used on.

   Everything here is inside the narrow breakpoint, so the pointer nav is
   untouched — the desktop masthead renders identically to before. The office
   application reuses .masthead/.nav/.navgroup and overrides only colour, so
   it inherits this; the button takes `currentColor` rather than an ink token
   for exactly that reason, and reads correctly against its dark masthead.

   See app/web/static/js/nav.js for why the closed state is guarded on
   `html:not(.no-js)` rather than applied unconditionally. */

.nav-toggle {
  display: none;              /* a pointer needs no button */
  align-items: center;
  justify-content: center;
  gap: 7px;
  padding: 8px 12px;
  margin-left: auto;
  background: none;
  border: 1px solid var(--line);
  border-radius: 8px;
  color: inherit;
  font: inherit;
  font-size: 0.85rem;
  cursor: pointer;
}
.nav-toggle:hover { background: var(--line-soft); }

/* Three bars that fold into an X. Drawn rather than set as a character so it
   takes the ink colour and depends on no font carrying the glyph. */
.nav-toggle .bars { position: relative; width: 16px; height: 10px; flex: none; }
.nav-toggle .bars::before,
.nav-toggle .bars::after,
.nav-toggle .bars span {
  content: "";
  position: absolute;
  left: 0;
  width: 100%;
  height: 1.5px;
  background: currentColor;
  border-radius: 2px;
  transition: transform 0.16s, opacity 0.16s;
}
.nav-toggle .bars::before { top: 0; }
.nav-toggle .bars span    { top: 4.25px; }
.nav-toggle .bars::after  { bottom: 0; }

.nav-toggle[aria-expanded="true"] .bars::before { transform: translateY(4.25px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] .bars span    { opacity: 0; }
.nav-toggle[aria-expanded="true"] .bars::after  { transform: translateY(-4.25px) rotate(-45deg); }

@media (max-width: 720px) {
  .nav-toggle { display: inline-flex; }

  .masthead-inner { flex-wrap: wrap; gap: 10px; }

  .nav {
    order: 3;
    width: 100%;
    flex-direction: column;
    align-items: stretch;
    gap: 1px;
    padding-top: 6px;
    border-top: 1px solid var(--line-soft);
  }

  /* Closed by default — but only where a script is present to reopen it. */
  html:not(.no-js) .nav[data-open="false"] { display: none; }

  .nav > a,
  .navgroup > a {
    display: block;
    padding: 11px 12px;        /* a thumb, not a cursor */
    border-radius: 7px;
    font-size: 0.95rem;
  }

  /* A group stops being a dropdown and becomes a heading with its list under
     it. The absolute positioning and the hover bridge are undone explicitly:
     both are pointer behaviour, and both misbehave in a vertical panel. */
  .navgroup { display: block; }
  .navgroup::after { content: none; }
  .navgroup .caret { display: none; }

  .navmenu {
    display: block;            /* always open inside the panel */
    position: static;
    min-width: 0;
    padding: 0 0 4px 12px;
    margin: 0 0 2px 10px;
    background: none;
    border: none;
    border-left: 1px solid var(--line);
    border-radius: 0;
    box-shadow: none;
  }
  .navmenu a { padding: 9px 10px; font-size: 0.9rem; color: var(--ink-soft); }
  .navsep { margin: 4px 10px; }
}

/* The one nav entry that opens a new tab (Inventory → Art). Marked in the menu
   rather than left to surprise: every other entry in this application navigates
   in place, and the art gallery is a separate shell with its own stylesheet, so
   the arrow is the honest signal that clicking leaves this page behind.
   Additive only — no existing selector is changed, and the office does not
   render this menu. */
.navmenu .ext{font-size:10px;opacity:.55;margin-left:2px;vertical-align:1px}

/* -----------------------------------------------------------------------
   The way back
   -----------------------------------------------------------------------
   Pairs with _back.html. The existing `.crumb` is a fine desktop breadcrumb
   and a poor thing to hit with a thumb — 0.82rem text on a 20px line — and
   on a phone this is not decoration but the only way out of the page: the
   installed app has no browser chrome, so a task opened from a house had no
   route back to that house at all.

   So it grows rather than shrinks at the narrow end, which is the opposite of
   what a breadcrumb usually does, and is the whole point. */

.backlink {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  margin-bottom: 12px;
  padding: 5px 10px 5px 6px;
  margin-left: -6px;          /* the text lines up with the heading below it */
  border-radius: 8px;
  font-size: 0.85rem;
  color: var(--ink-soft);
  transition: background 0.12s, color 0.12s;
}
.backlink:hover { background: var(--line-soft); color: var(--ink); }
.backlink-arrow { font-size: 1.05em; line-height: 1; opacity: 0.75; }

@media (max-width: 720px) {
  /* A full row, at the top of the page, at a size a thumb finds without
     aiming — this is the back button the installed app does not have. */
  .backlink {
    display: flex;
    margin: 0 0 16px;
    padding: 11px 12px;
    border: 1px solid var(--line);
    background: var(--paper-raised);
    font-size: 0.92rem;
    color: var(--ink);
  }
  .backlink-arrow { opacity: 1; }
}

/* -----------------------------------------------------------------------
   The count on a work group
   -----------------------------------------------------------------------
   "How many" was a phrase inside a sentence, which is a fine thing to read
   and a poor thing to glance at. As a badge it survives being scanned down a
   column of groups, and it leaves the summary line free to say something
   that is not a number. */

.workgroup .wg-count {
  flex: none;
  min-width: 24px;
  padding: 1px 7px;
  border-radius: 999px;
  background: var(--line-soft);
  color: var(--ink-soft);
  font-size: 0.8rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  text-align: center;
}

/* The summary is a sentence now rather than two words, so it needs room and
   it must not shove the count onto its own line on a narrow screen. */
.workgroup > summary .wg-name { flex: 1 1 auto; min-width: 0; }

@media (max-width: 560px) {
  /* Only the loose group's tail goes. There it repeats what the badge beside
     it already says, so it is the cheapest thing to drop when three items do
     not fit on a phone.

     Everywhere else it is the only thing distinguishing one group from
     another: five runs of "Hamptons weekend" are five identical rows without
     "· 19 Sep · 1 of 6 done", and hiding it turned a list into a guess. That
     is what this rule did before `data-key` existed to say which group is
     which. */
  .workgroup[data-key="loose"] > summary .wg-meta { display: none; }
}

/* -----------------------------------------------------------------------
   Sections inside the phone menu
   -----------------------------------------------------------------------
   The panel opened with every group expanded — deliberately, because a tap is
   not a hover and a dropdown that needs two taps strands people. What that
   traded away was length: five groups fully open is about thirty rows, so
   finding Vendors meant scrolling past Storage, Groceries, Restock, Donations
   and Valuables to get there. Being reachable is not the same as being
   findable.

   So a group collapses. What makes this safe is a fact about this menu rather
   than a general principle: in every group, the label's own destination is
   already the first item in its list — Houses leads to Properties, Tasks to
   All tasks, People to Staff directory, More to Agent activity, Inventory to
   Manage inventory. Nothing is behind the fold that is not also on the row
   above it, so a collapsed group hides no destination.

   The label stays a link. The chevron beside it is a separate button with its
   own hit area, injected by nav.js — so "go to Houses" and "show me what is
   under Houses" are two different targets rather than one guess. Both are
   full-height rows to hit with a thumb.

   Injected rather than written into base.html because this is behaviour, not
   structure: where the script does not run there is no button, every group
   stays open, and the menu is exactly what it was. */

.navgroup-toggle { display: none; }

@media (max-width: 720px) {
  .navgroup { display: flex; flex-wrap: wrap; align-items: center; }
  .navgroup > a { flex: 1 1 auto; }

  .navgroup-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    width: 44px;
    align-self: stretch;
    min-height: 42px;
    padding: 0;
    border: 0;
    border-radius: 7px;
    background: none;
    color: var(--ink-faint);
    font: inherit;
    font-size: 0.7rem;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
  }
  .navgroup-toggle:hover { background: var(--line-soft); color: var(--ink); }
  .navgroup-toggle .chev { transition: transform 0.15s ease; line-height: 1; }
  .navgroup-toggle[aria-expanded="true"] .chev { transform: rotate(180deg); }

  /* The list is a flex item of the group, forced onto its own line. */
  .navgroup .navmenu { flex: 0 0 100%; }
  .navgroup[data-sub="closed"] .navmenu { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .navgroup-toggle .chev { transition: none; }
}

/* -----------------------------------------------------------------------
   The way into the thing a group is named after
   -----------------------------------------------------------------------
   A routine group is named for a routine and a project group for a project,
   so the name should lead somewhere. This link lived at the foot of the
   expanded body, under every task in the run — the one place you cannot see
   it while deciding whether to open the group at all.

   In the header it has to earn its space without competing with the name, so
   it sits at the far end, quiet until approached. It is a real link inside a
   <summary>: the browser follows it, and workgroups.js stops the click also
   reaching the summary and folding the group on the way past. */

.workgroup > summary .wg-open {
  margin-left: auto;
  flex: none;
  padding: 3px 8px;
  border-radius: 6px;
  font-size: 0.82rem;
  color: var(--ink-faint);
  white-space: nowrap;
  transition: background 0.12s, color 0.12s;
}
.workgroup > summary .wg-open:hover { background: var(--accent-soft); color: var(--accent); }

@media (max-width: 560px) {
  /* Its own line rather than squeezed beside the name, and a full row to hit
     with a thumb — this is a destination, not an ornament. */
  .workgroup > summary .wg-open {
    margin-left: 0;
    flex-basis: 100%;
    padding: 8px 0 2px;
    color: var(--accent);
  }
}

/* -----------------------------------------------------------------------
   Home
   -----------------------------------------------------------------------
   The front page, rebuilt as one screen. Cards over sections, one-line rows
   over stacked tables, and nothing that renders empty. The page's history is
   in home.html's comments; this block is only its clothes.

   Additive by the coordination rule: no existing selector changes. */

.home-head {
  display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap;
  margin-bottom: 20px;
}
.home-hello { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
.home-head h1 { font-family: var(--serif); font-weight: 500; font-size: 1.5rem; }
.home-date { color: var(--ink-faint); font-size: 0.85rem; }
.home-actions { margin-left: auto; display: flex; gap: 8px; }

/* Three columns that become one interleaved column on a phone. The wrappers
   keep the desktop reading order; display:contents dissolves them under
   900px so the cards can reorder by importance instead of by column. */
/* minmax(0, …fr) rather than bare fr: an fr track's floor is its content's
   min-width, so one nowrap string would widen its whole column past the
   viewport. Zero floors keep the columns honest and push the truncation down
   into the rows, where the ellipsis rules are. */
/* Full width, above the three-column grid: the For-you card reads
   cramped at a third of the screen, and it is the reason anyone opens
   the app. Same bottom margin as the grid's own row gap, so the card
   below it lines up exactly as if this were one more grid row. */
.foryou-wrap { margin-bottom: 18px; }

.home-grid { display: grid; grid-template-columns: minmax(0,1.15fr) minmax(0,1fr) minmax(0,0.95fr); gap: 18px; align-items: start; }
.hcol { min-width: 0; display: grid; gap: 18px; align-content: start; }

.hcard {
  background: var(--paper-raised); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 14px 16px;
  /* The card is the clipping boundary. Without the cap, one long nowrap
     string inside a row — a shift list, a note snippet — sets the card's
     intrinsic width and the whole page grows sideways: percentages cannot
     resolve while intrinsic width is being computed, so every max-width:38%
     inside is momentarily no limit at all. Capping the card makes its width
     definite, which is what lets those percentages work. */
  max-width: 100%; min-width: 0; overflow: hidden;
}
.hcard-head {
  display: flex; align-items: baseline; gap: 9px;
  padding-bottom: 8px; margin-bottom: 4px;
  border-bottom: 1px solid var(--line-soft);
}
.hcard-head h2 { font-family: var(--serif); font-weight: 500; font-size: 1.05rem; }
.hcard-head .hn {
  flex: none; min-width: 22px; text-align: center;
  padding: 0 7px; border-radius: 999px;
  background: var(--line-soft); color: var(--ink-soft);
  font-size: 0.76rem; font-weight: 600; font-variant-numeric: tabular-nums;
}
.hcard-head .more { margin-left: auto; font-size: 0.78rem; color: var(--ink-faint); white-space: nowrap; }
a.more:hover { color: var(--accent); }

/* One line per fact. The old queue table spent four stacked rows per task on
   a phone — including rows that only said "—". */
.qrows { list-style: none; margin: 0; padding: 0; }
.qrows li {
  display: flex; align-items: baseline; gap: 8px;
  padding: 8px 0; border-bottom: 1px solid var(--line-soft);
  font-size: 0.88rem; min-width: 0;
}
.qrows li:last-child { border-bottom: 0; }

/* A note row's own answers: Read, and Reply. Pushed to the right end of the
   line so the row still reads title-then-facts left to right, and `flex: none`
   because these must never be what the ellipsis eats — a truncated detail is
   a cosmetic loss, a truncated button is a dead control. Coordination note:
   this is a HouseOps-only block; the office loads app.css but renders no
   .qrows, so nothing over there moves. */
.rowacts { flex: none; margin-left: auto; display: flex; gap: 6px; align-items: center; }
.qrows li .rowacts .btn { white-space: nowrap; }

/* The reply box, as its own row under the note it answers. A sibling rather
   than a child of the row for the reason the template gives: the rows are
   flex lines and a text field inside one fights the truncation rules. */
/* `hidden` has to win here. The attribute's own rule is display:none at the
   bottom of the cascade, and `.qrows li { display: flex }` outranks it by
   specificity — so the reply row stayed visible with hidden="" set on it,
   which looked like the script never ran. Found in the browser, not by any
   test: nothing here asserts appearance. */
.qrows li.replyrow[hidden] { display: none; }
.qrows li.replyrow { border-bottom: 0; padding-top: 0; }
/* 16px, not the global 0.9rem (dd9339a8, the owner's screenshot): Reply
   focuses this field, and iOS Safari ZOOMS any focused input under 16px —
   then Cancel hides the box and leaves the zoom, which is the "layout broken
   after Cancel" he photographed. The zoom never fires at 16px; the same rule
   docs/mobile-native-ux.md wrote down for the Move's inputs. */
.qrows .replyform input[name="body"] { font-size: 16px; }
.replyform { display: flex; gap: 6px; width: 100%; align-items: center; }
.replyform input[name="body"] {
  flex: 1 1 auto; min-width: 0;
  padding: 7px 10px; border: 1px solid var(--line); border-radius: 6px;
  background: var(--paper-raised); color: var(--ink); font-size: 0.88rem;
}
.replyform input[name="body"]:focus { outline: none; border-color: var(--accent); }

@media (max-width: 720px) {
  /* On a phone the actions drop under the line rather than squeezing it: a
     47-character note title and two buttons do not share 375px. */
  .qrows li { flex-wrap: wrap; }
  .rowacts { margin-left: 0; width: 100%; padding-top: 6px; }
  .replyform { flex-wrap: wrap; }
  .replyform input[name="body"] { flex: 1 1 100%; }
}
.qrows li > a, .qrows .qtitle {
  flex: 1 1 auto; min-width: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.qrows li > a:hover { color: var(--accent); }
.qrows .pill { flex: none; }
.qwhere {
  flex: none; max-width: 38%;
  color: var(--ink-faint); font-size: 0.76rem;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.qempty { color: var(--ink-faint); font-size: 0.84rem; padding: 8px 0 2px; }

.refrow { display: flex; gap: 8px; flex-wrap: wrap; padding-top: 6px; }
.refrow a {
  font-size: 0.82rem; padding: 6px 12px; border-radius: 999px;
  border: 1px solid var(--line); background: var(--paper-raised);
  color: var(--ink-soft);
}
.refrow a:hover { color: var(--accent); border-color: var(--accent); }

/* ---------- the home grid on a phone ----------
   The capture dock that lived here from the home page's birth is gone: the
   household thumb bar (components.css + _thumbbar.html) is its
   generalization, carrying the same instruments with the same hrefs on
   every page rather than one. */

@media (max-width: 720px) {
  /* The For-you card sits above .home-grid now (full width on every
     breakpoint), so it needs no order here — it is already first in the
     document. What remains inside the grid keeps the flatten-and-flex
     rule so its cards can still interleave by source order. */
  .home-grid { display: flex; flex-direction: column; }
  .hcol { display: contents; }
  .c-clock { order: 1; }
  .c-ontoday { order: 2; }
  .c-shifts { order: 3; }
  .c-refs { order: 4; }

  .home-actions { display: none; }
}

@media (min-width: 721px) and (max-width: 980px) {
  .home-grid { grid-template-columns: minmax(0,1fr) minmax(0,1fr); }
}

/* The bubble-above-the-bar rule moved to components.css with the bar itself:
   body:has(.thumbbar) covers every household page now, not just home. */

/* The day clock, as a Home card. Reuses the old /day page's own vocabulary
   (.daystate/.dayclock/.dayform) so the clock looks like itself in its new
   home. The dot finally gets ink — day.html drew an unstyled span for a year
   and nobody saw it, because there was nothing to see. */
.c-clock { text-align: center; }
.c-clock .dayform { text-align: left; }
.c-clock .dot {
  display: inline-block; width: 9px; height: 9px; border-radius: 50%;
  background: var(--line); flex: none;
}
.c-clock .dot.live { background: var(--good); }
.c-clock .check { display: flex; align-items: center; gap: 8px; font-size: 0.86rem; }

/* The one capture destination on /voice that is not the microphone or the
   camera. A quiet link rather than a third button, because those two are modes
   of one flow and art is a different page — see the comment in voice.html. */
.voice-elsewhere{
  display:inline-block;margin-top:14px;font-size:0.85rem;
  color:var(--ink-soft);text-decoration:underline;text-underline-offset:3px;
}
.voice-elsewhere:hover{color:var(--ink)}

/* ---------- groceries (059) ----------
   Five small additions, all scoped to class names nothing else uses. app.css is
   shared with the office (office/base.html loads it), so a change here is a
   coordination event — these add rather than alter, and no existing component
   moves. See CLAUDE.md on why appearance is the gap that nothing tests.

   .tabrow is the one worth a word: the houses are the four things the groceries
   page is about, and a row that says "there are four of these" is doing work a
   select cannot. Built from the same tokens as .nav a, deliberately, so a house
   tab and a nav item are recognisably the same kind of control. */

.tabrow {
  display: flex;
  gap: 4px;
  flex-wrap: wrap;
  margin-bottom: 4px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--line);
}
.tabrow .tab {
  padding: 7px 13px;
  border-radius: 7px;
  font-size: 0.9rem;
  color: var(--ink-soft);
  display: inline-flex;
  align-items: center;
  gap: 7px;
  transition: background 0.12s, color 0.12s;
}
.tabrow .tab:hover { background: var(--line-soft); color: var(--ink); }
.tabrow .tab.active { background: var(--accent-soft); color: var(--accent); font-weight: 500; }

/* The order's footer: the way on to placing it, with the sentence that says who
   may. Not a card, because it belongs to the table above it. */
.orderfoot {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
  margin-top: 14px;
}

/* Adding a line by name. A row of fields that wraps, the same shape the vendor
   and asset forms use, because a grocery line is three short values and a
   column of them reads as a chore. */
.addline {
  display: flex;
  gap: 12px;
  align-items: flex-end;
  flex-wrap: wrap;
  margin-top: 18px;
  padding-top: 16px;
  border-top: 1px solid var(--line-soft);
}
.addline .f { display: flex; flex-direction: column; gap: 5px; }
.addline .f.grow { flex: 1 1 190px; }
.addline .f.narrow { flex: 0 0 110px; }

/* What a house keeps, inside its place card. Tight rows, because the value of
   this list is being able to scan it for what is missing. */
ul.keeps { list-style: none; display: flex; flex-direction: column; gap: 6px; margin-top: 10px; }
ul.keeps li {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  font-size: 0.86rem;
  align-items: baseline;
}

/* The shopping list as text. Monospaced and selectable: it exists to be copied
   into whichever app is doing the shopping, and a proportional font makes a
   pasted list look like prose. */
.shoplist {
  width: 100%;
  font-family: var(--mono);
  font-size: 0.82rem;
  line-height: 1.6;
  background: var(--paper-raised);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 14px 16px;
  color: var(--ink);
  resize: vertical;
}

/* The quantity, edited where it is read. Narrow, so the field reads as a number
   rather than as a text box that happens to hold one. */
form.qty { display: inline-flex; align-items: center; gap: 6px; }
form.qty input { width: 68px; font-size: 0.85rem; padding: 5px 8px; }

/* A reading column with a narrow aside — the order page's shape. A CLASS and
   not an inline style, because an inline grid-template-columns outranks every
   media query below it: the first version of the order page carried one, and at
   phone width the aside kept its 320px while the order itself was computed down
   to nine pixels. The collapse rule is the fix; the class is what lets it
   apply. */
.grid.aside { grid-template-columns: minmax(0, 1fr) 320px; }
@media (max-width: 860px) {
  .grid.aside { grid-template-columns: 1fr; }
}

/* ---------- icons (phase 1) ----------
   The whole icon system is two classes. Icons are 1em so they scale with the
   type beside them, and currentColor so a faint menu entry, an accent-lit
   active state and a warn pill all color their icon by coloring their text —
   which is also what makes both themes work with one set of drawings. The only
   fill in the system is the keystone dot, the brand mark's own device.
   Shared file: additive only, nothing existing moves. */
.icn { width: 1em; height: 1em; stroke: currentColor; fill: none;
       stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round;
       vertical-align: -0.14em; }
.icn .dot { fill: currentColor; stroke: none; }
.feed-kind .icn { font-size: 1.25em; margin-right: 2px; }
.navmenu a .icn { font-size: 15px; margin-right: 9px; color: var(--ink-faint);
                  vertical-align: -0.16em; }
.navmenu a:hover .icn { color: inherit; }
.page-head h1 .icn { font-size: 0.72em; color: var(--ink-faint);
                     margin-right: 10px; vertical-align: -0.06em; }
.empty .icn { display: block; margin: 0 auto 10px; font-size: 26px;
              color: var(--ink-faint); }

/* ---------- scheduling surfaces ----------
   ADDITIVE ONLY, new class names, appended after the icons block. Nothing
   existing moves, which matters because the family office loads this file and
   nothing tests how its pages look (CLAUDE.md). No `.card`, `.grid`,
   `.table-wrap`, `.rowform`, `.pill`, `.section-head` or spacing token is
   touched here.

   `.mecal` is the staff member's own three months. A month grid rather than the
   day-chip rail `.daystrip` uses: that rail is right for a fortnight and
   unusable at ninety days, while seven fixed columns fit a 375px phone at about
   53px each. Fixed layout so a cell with two shifts in it does not widen its
   whole column and shove December off the screen. */
table.mecal { table-layout: fixed; width: 100%; font-size: 0.8rem; }
table.mecal th { padding: 4px 2px; text-align: center; font-size: 0.72rem;
                 color: var(--ink-faint); font-weight: 500; }
table.mecal th.weekend { background: var(--line-soft); }
.mecal-day { vertical-align: top; padding: 0; height: 54px;
             border: 1px solid var(--line-soft); }
.mecal-day.weekend { background: color-mix(in srgb, var(--line-soft) 45%, transparent); }
.mecal-day.mecal-blank { border-color: transparent; background: none; }
/* The whole cell is the tap target. A bare date is a fiddly thing to hit on a
   phone, and the day page is the only destination a cell has. */
.mecal-day > a { display: block; height: 100%; padding: 4px 4px 5px;
                 text-decoration: none; color: inherit; }
.mecal-day > a:hover { background: var(--paper-raised); }
.mecal-day.today { outline: 2px solid var(--accent); outline-offset: -2px; }
.mecal-day .dom { display: block; font-size: 0.74rem; color: var(--ink-faint);
                  line-height: 1.1; }
.mecal-day.today .dom { color: var(--accent); font-weight: 600; }
/* One line per shift, and the time is the whole label — a house name does not
   fit in 53px and is on the day page and in the title attribute instead. */
.mecal .mshift { display: block; margin-top: 2px; padding: 1px 3px;
                 background: var(--accent-soft); color: var(--ink);
                 border-radius: 3px; font-family: var(--mono);
                 font-size: 0.66rem; line-height: 1.35;
                 white-space: nowrap; overflow: hidden; }
/* Time off sits BESIDE a shift rather than replacing it. The rota page's old
   grid overwrote one with the other, which is how a day that is both "scheduled"
   and "asked off" reads as settled when it is not. */
.mecal .moff { display: block; margin-top: 2px; padding: 1px 3px;
               background: var(--warn-soft); color: var(--warn);
               border-radius: 3px; font-size: 0.64rem; line-height: 1.35;
               font-style: italic; white-space: nowrap; overflow: hidden; }
/* A dotted outline and no fill: something is asked for and not yet answered, so
   it must not look like a decision. */
.mecal .mpend { display: inline-block; margin-top: 2px; min-width: 14px;
                padding: 0 3px; border: 1px dotted var(--accent);
                border-radius: 3px; color: var(--accent);
                font-size: 0.62rem; line-height: 1.4; text-align: center; }

@media (max-width: 520px) {
  /* Tighter, not scrollable: a calendar you have to scroll sideways stops being
     a calendar. The date and the first shift time survive; a second shift
     becomes a second line and the row grows, which is the right trade. */
  table.mecal { font-size: 0.74rem; }
  .mecal-day { height: 48px; }
  .mecal-day > a { padding: 3px 2px 4px; }
  .mecal .mshift, .mecal .moff { font-size: 0.6rem; padding: 0 2px; }
}

/* The review queue and the publication banner. Same inbox reasoning as
   `.notelist`: rows sharing one border, the thing the row is about on the line
   the eye is already on, and the metadata that rarely decides anything at the
   end. Not reusing `.notelist` itself — these rows carry a form, and widening
   that class to fit would change the notes page too. */
.pubbanner { display: flex; align-items: center; justify-content: space-between;
             gap: 12px; flex-wrap: wrap; margin: 0 0 16px; padding: 10px 14px;
             background: var(--paper-raised); border-left: 3px solid var(--accent);
             border-radius: 4px; font-size: 0.88rem; }
.reviewq { border-top: 1px solid var(--line-soft); }
.revrow { padding: 11px 0; border-bottom: 1px solid var(--line-soft); }
.revmeta { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
           font-size: 0.86rem; }
.revwho { font-weight: 500; text-decoration: none; color: var(--ink); }
.revwho:hover { text-decoration: underline; }
.revwhen { font-weight: 500; }
.revasked { font-size: 0.78rem; margin-left: auto; }
.revbody { margin-top: 3px; font-size: 0.9rem; display: flex; gap: 8px;
           flex-wrap: wrap; align-items: baseline; }
.revnote { font-style: italic; color: var(--ink-soft); }
.revdecide { display: flex; gap: 8px; align-items: center; margin-top: 7px;
             flex-wrap: wrap; }
.revdecide input[name="note"] { flex: 1 1 240px; min-width: 0; }

@media (max-width: 600px) {
  /* The "asked by" line stops being pushed to the right, where at this width it
     would wrap onto its own line anyway and look like a stray fragment. */
  .revasked { margin-left: 0; }
  .revdecide input[name="note"] { flex-basis: 100%; }
}

/* The catalog's front door on the groceries page, and the photo-capture tiles
   (shared markup in _photo_shot.html; art styles the same classes in its own
   stylesheet with its own identity — the two pages do not share CSS, only
   behaviour). */
.catalog-lead {
  display: flex;
  align-items: center;
  gap: 16px;
  background: var(--paper-raised);
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius);
  padding: 16px 20px;
  margin-bottom: 22px;
  box-shadow: var(--shadow);
  transition: border-color 0.14s, transform 0.14s;
}
.catalog-lead:hover { border-color: var(--accent); transform: translateY(-1px); }
.catalog-lead > .icn { font-size: 30px; color: var(--accent); flex-shrink: 0; }
.catalog-lead-text { flex: 1; min-width: 0; }
.catalog-lead-text strong { font-family: var(--serif); font-size: 1.12rem; font-weight: 500; display: block; }
.catalog-lead-text span { font-size: 0.86rem; color: var(--ink-soft); }
.catalog-lead-count { font-size: 0.8rem; color: var(--ink-faint); white-space: nowrap; }

.shot-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
@media (max-width: 640px) { .shot-row { grid-template-columns: 1fr; } }
.shot { display: block; cursor: pointer; }
.shot-face {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 7px; padding: 30px 18px; border: 1.5px dashed var(--line);
  border-radius: var(--radius); background: var(--paper-raised); text-align: center;
  transition: border-color 0.15s, background 0.15s;
}
.shot:hover .shot-face, .shot-face:focus-within { border-color: var(--accent); }
.shot-face .icn { color: var(--accent); }
.shot-face strong { font-family: var(--serif); font-size: 1.05rem; font-weight: 500; }
.shot-face em { font-size: 0.8rem; color: var(--ink-faint); font-style: normal; }
.strip { display: flex; gap: 9px; flex-wrap: wrap; margin-top: 12px; }
.strip .shotcell { position: relative; }
.strip img { width: 84px; height: 84px; object-fit: cover; border-radius: 8px;
             border: 1px solid var(--line); display: block; }
.shotdrop {
  position: absolute; top: -7px; right: -7px; width: 22px; height: 22px;
  border-radius: 50%; border: 1px solid var(--line); background: var(--paper-raised);
  color: var(--ink-soft); font-size: 13px; line-height: 1; cursor: pointer;
}

/* ---------- the board ----------
   Stacked WEEK tables: seven fixed columns, people down, coverage in the foot.
   `table.rota` is the other shape — one table of 14 to 42 date columns — and the
   reason this is not that: at 42 columns the far weeks are off the side of the
   screen, the week boundaries vanish, and every column is starved of width. Seven
   columns fit a 1280px viewport at about 150px, which is room for two stacked
   chips, and the multi-week dimension becomes vertical scroll — the direction
   people already have. Scoped to `.schedweek` throughout so nothing here touches
   the rota. */
.boardnav { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.boardweeks { display: inline-flex; gap: 2px; margin-left: 4px; }
.boardweeks a { padding: 3px 7px; font-size: 0.78rem; border-radius: 4px;
                text-decoration: none; color: var(--ink-soft);
                border: 1px solid var(--line-soft); }
.boardweeks a.on { background: var(--accent-soft); color: var(--accent);
                   border-color: var(--accent); }

.weekband { display: flex; align-items: baseline; gap: 10px; margin: 22px 0 6px;
            border-bottom: 2px solid var(--line); padding-bottom: 4px; }
.weekband h2 { margin: 0; font-size: 1rem; }
.weekband .meta { font-size: 0.8rem; color: var(--ink-faint); }

/* Each week scrolls on its own. Survivable on a phone precisely because a week is
   seven columns: the rota's forty-two are what made horizontal scroll useless. */
.schedweek-wrap { overflow-x: auto; }

/* The board's two presentations (mobile plan, Phase 3): the grid is the
   Director's desk; a phone gets each week TRANSPOSED into day cards — a
   Director on a phone asks "is Thursday covered", not "what is Nina's
   fortnight". Same rows/coverage structure renders both, so they cannot
   disagree. The shift chips keep their property colors and the legend
   explains them, exactly as on the grid. */
.boardagenda { display: none; }
@media (max-width: 720px) {
  .schedweek-wrap { display: none; }
  /* Re-plumbing calendars is desk work; the phone visit is reading the
     board and answering days. */
  .boardplumbing { display: none; }
  .boardagenda { display: block; list-style: none; margin: 0 0 20px; padding: 0; }
  .ba-day {
    display: grid; grid-template-columns: 58px minmax(0, 1fr);
    gap: 2px 10px; padding: 7px 2px;
    border-bottom: 1px solid var(--line-soft);
  }
  .ba-day.today .ba-date { color: var(--accent); font-weight: 600; }
  .ba-day.weekend .ba-date { opacity: 0.65; }
  .ba-date { font-size: 0.82rem; font-weight: 500; white-space: nowrap; padding-top: 5px; }
  .ba-body { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
  .ba-person {
    display: flex; flex-wrap: wrap; gap: 5px 7px; align-items: center;
    min-height: var(--tap); text-decoration: none; color: inherit;
    -webkit-tap-highlight-color: transparent;
  }
  .ba-name { font-size: 0.85rem; font-weight: 500; }
  .ba-gap {
    display: inline-flex; gap: 6px; align-items: center;
    font-size: 0.78rem; color: var(--warn); padding: 4px 0;
  }
}
table.schedweek { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
table.schedweek th, table.schedweek td { border: 1px solid var(--line-soft);
                                         padding: 0; vertical-align: top; }
table.schedweek th { padding: 5px 6px; font-size: 0.74rem; font-weight: 500;
                     color: var(--ink-faint); text-align: center; }
table.schedweek th.weekend, table.schedweek td.weekend {
  background: color-mix(in srgb, var(--line-soft) 40%, transparent); }
table.schedweek th.today { background: var(--accent-soft); color: var(--accent); }
table.schedweek .who { text-align: left; white-space: nowrap; padding: 6px 8px;
                       font-size: 0.84rem; min-width: 140px; }
table.schedweek .who a { text-decoration: none; color: var(--ink); }
table.schedweek .who a:hover { text-decoration: underline; }
table.schedweek .who .dim { display: block; font-size: 0.72rem; }
.bcell { min-width: 108px; }
.bcell.today { box-shadow: inset 0 0 0 1px var(--accent); }
/* The whole cell is the link, so the tap target is the cell and not a word in it. */
.bfill { display: block; min-height: 38px; padding: 4px 5px; text-decoration: none;
         color: inherit; }
a.bfill:hover { background: var(--paper-raised); }
.bshift { display: block; margin-bottom: 2px; padding: 1px 4px; border-radius: 3px;
          font-family: var(--mono); font-size: 0.7rem; line-height: 1.4;
          white-space: nowrap; border-left: 3px solid transparent; }
.boff { display: block; padding: 1px 4px; border-radius: 3px;
        background: var(--warn-soft); color: var(--warn); font-size: 0.68rem;
        font-style: italic; line-height: 1.4; }
/* Dotted and unfilled: something is asked and unanswered, so it must not read as
   a decision that has been made. */
.basked { display: block; margin-top: 2px; padding: 0 4px; border-radius: 3px;
          border: 1px dotted var(--accent); color: var(--accent);
          font-size: 0.66rem; line-height: 1.4; }

.covrow td { border-top: 2px solid var(--line); }
.covrow .who { font-size: 0.78rem; color: var(--ink-soft); }
.covcell { text-align: center; font-family: var(--mono); font-size: 0.74rem;
           padding: 4px 0; color: var(--ink-soft); }
/* A house with nobody at it on a day something is happening. The absence is the
   thing worth noticing — `.card.uncovered` on /schedule makes the same promise. */
.covcell.cov0 { background: var(--warn-soft); color: var(--warn); font-weight: 600; }
.covdot { display: inline-block; width: 8px; height: 8px; border-radius: 2px;
          margin-right: 6px; vertical-align: 0; }

.proplegend { display: flex; gap: 6px; flex-wrap: wrap; margin: 0 0 12px;
              font-size: 0.76rem; }
.propchip { padding: 1px 7px; border-radius: 3px; border-left: 3px solid transparent; }
.propchip.legend-off { background: var(--warn-soft); color: var(--warn);
                       font-style: italic; }
.propchip.legend-asked { border: 1px dotted var(--accent); color: var(--accent); }

/* Six houses, six pairs, assigned by creation order so a rename never moves
   anybody's colour. Hand-chosen rather than generated: a household has five
   houses, and five legible accents beat an algorithm whose seventh value is
   unreadable. `prop-none` is a shift with no house set — visible, unnamed. */
.prop-1, .covdot.prop-1 { background: #e8eef7; border-left-color: #5b7fa8; }
.prop-2, .covdot.prop-2 { background: #eaf3ec; border-left-color: #5f9068; }
.prop-3, .covdot.prop-3 { background: #f6eee7; border-left-color: #a87b52; }
.prop-4, .covdot.prop-4 { background: #f1ecf5; border-left-color: #7d6295; }
.prop-5, .covdot.prop-5 { background: #fbf0ee; border-left-color: #a86a5b; }
.prop-6, .covdot.prop-6 { background: #eef4f5; border-left-color: #55868c; }
.prop-none { background: var(--line-soft); border-left-color: var(--ink-faint); }

@media (prefers-color-scheme: dark) {
  /* Darker grounds at the same hues, so the chips stay distinguishable without
     the left rules — which carry the identity — needing to change at all. */
  .prop-1, .covdot.prop-1 { background: #1e2733; }
  .prop-2, .covdot.prop-2 { background: #1d2a20; }
  .prop-3, .covdot.prop-3 { background: #2c231a; }
  .prop-4, .covdot.prop-4 { background: #262034; }
  .prop-5, .covdot.prop-5 { background: #2f211e; }
  .prop-6, .covdot.prop-6 { background: #1c2829; }
}

@media (max-width: 700px) {
  table.schedweek .who { min-width: 104px; font-size: 0.78rem; }
  .bcell { min-width: 82px; }
  .bshift { font-size: 0.64rem; }
}

/* ---------- the catalog's product overlay ----------
   `.lightbox` and `.lightbox-close` are NOT redefined here — _gallery.html has
   owned them since the art module and this reuses them, because a second
   full-viewport viewer in one stylesheet is how two overlays drift apart. What
   is genuinely new is the panel BESIDE the photograph: this overlay exists to
   say what the product IS, not only to show it larger. */
.catlb-inner { display: flex; gap: 20px; align-items: flex-start;
               max-width: min(1100px, 92vw); max-height: 86vh; }
.catlb-inner > img { max-width: min(620px, 55vw); max-height: 86vh;
                     object-fit: contain; border-radius: 4px;
                     background: var(--paper); }
.catlb-side { flex: 1 1 260px; min-width: 240px; max-height: 86vh;
              overflow-y: auto; color: var(--paper);
              /* The overlay ground is dark; the panel's text is the paper
                 colour rather than --ink for that reason, and the two are
                 defined per theme so this holds in both. */
              padding-right: 4px; }
.catlb-side h3 { margin: 0 0 4px; font-size: 1.1rem; color: var(--paper); }
.catlb-side .dim { color: color-mix(in srgb, var(--paper) 62%, transparent); }
.catlb-side .link { color: var(--paper); text-decoration: underline; }

/* The model's reading. Three states and they are different things to say, so
   they look different: prose, an honest absence, and a hedge that must not be
   dressed up as a description. */
.catlb-notes { margin: 12px 0; padding: 10px 12px; border-radius: 4px;
               background: color-mix(in srgb, var(--paper) 12%, transparent);
               border-left: 3px solid color-mix(in srgb, var(--paper) 45%, transparent);
               font-size: 0.9rem; line-height: 1.5; white-space: pre-wrap; }
.catlb-notes.empty { font-style: italic;
                     color: color-mix(in srgb, var(--paper) 55%, transparent);
                     border-left-color: color-mix(in srgb, var(--paper) 25%, transparent); }
.catlb-notes.unsure { border-left-color: var(--warn); }
.catlb-when { display: block; margin-top: 8px; font-size: 0.76rem;
              color: color-mix(in srgb, var(--paper) 55%, transparent); }
.catlb-describe { margin: 10px 0 14px; display: flex; gap: 8px;
                  align-items: baseline; flex-wrap: wrap; }
.catlb-describe .hint { font-size: 0.76rem; margin: 0;
                        color: color-mix(in srgb, var(--paper) 55%, transparent); }

@media (max-width: 760px) {
  /* Stacked, and the image gives up height so the description is on screen
     without scrolling past a picture the person has already seen. */
  .catlb-inner { flex-direction: column; gap: 12px; overflow-y: auto; }
  .catlb-inner > img { max-width: 100%; max-height: 42vh; }
  .catlb-side { max-height: none; min-width: 0; }
}

/* ---------------------------------------------------------------------------
   The staff-scheduling tab strip (_schedule_tabs.html): one identity over the
   five surfaces that used to be five menu entries. Lived briefly as a <style>
   block inside the partial while this file was mid-edit in two other sessions;
   moved here the moment the queue cleared. The count pill is the queue state
   riding on a tab — warn-toned because it is a call to action, not a decoration.
--------------------------------------------------------------------------- */
.schedtabs { display: flex; align-items: baseline; gap: 4px; flex-wrap: wrap;
             margin: 0 0 14px; }
.schedtabs .kicker { font-size: 0.78rem; color: var(--ink-faint);
                     text-transform: uppercase; letter-spacing: 0.06em;
                     margin-right: 8px; }
.schedtabs a, .schedtabs .here {
    padding: 4px 10px; font-size: 0.85rem; border-radius: 6px;
    text-decoration: none; border: 1px solid var(--line-soft);
    color: var(--ink-soft); }
.schedtabs .here { background: var(--accent-soft); color: var(--accent);
                   border-color: var(--accent); font-weight: 600; }
.schedtabs .count { display: inline-block; min-width: 1.4em; margin-left: 5px;
                    padding: 0 4px; border-radius: 8px; text-align: center;
                    font-size: 0.75rem; font-weight: 700;
                    background: var(--warn-soft); color: var(--warn); }

/* ---------- the grocery house tabs, and what is waiting at each ----------
   The count sits ABOVE the name rather than beside it so the row of tabs reads
   as a row of houses first and a set of numbers second — the page's own argument
   is that the houses are the thing it is about, and a leading number would invert
   that. Additive: `.tab` itself is untouched, since it is used elsewhere. */
.tabrow .tab { text-align: center; }
.tabrow .tab .tabname { display: block; }
.ordercount { display: block; margin: 0 auto 2px; min-width: 20px;
              padding: 0 5px; border-radius: 9px;
              background: var(--accent); color: var(--paper);
              font-family: var(--mono); font-size: 0.68rem; line-height: 1.55;
              font-weight: 600; }
/* An order that exists with nothing on it: somebody has started a list. A dot
   rather than a 0, because "started and empty" and "no list" are different facts
   and the page has to be able to say both. */
.ordercount.empty { background: var(--line); color: var(--ink-soft);
                    font-weight: 400; }
.tabrow .tab.has-order { border-color: color-mix(in srgb, var(--accent) 45%, var(--line)); }

/* ---------- adding to an order from the catalog ----------
   The chooser is a `<details>`, collapsed, so five hundred rows cost five
   hundred summaries and nothing else until one is opened. No JavaScript, and the
   default single-house button beside it still works on its own. */
.addwhere { display: inline-block; }
.addwhere > summary { cursor: pointer; font-size: 0.78rem; color: var(--ink-soft);
                      list-style: none; padding: 2px 6px; border-radius: 4px;
                      border: 1px solid var(--line-soft); white-space: nowrap; }
.addwhere > summary::-webkit-details-marker { display: none; }
.addwhere > summary:hover { background: var(--paper-raised); }
.addwhere[open] > summary { background: var(--paper-raised); }
.addwhere-panel { position: absolute; z-index: 30; margin-top: 4px;
                  padding: 10px 12px; min-width: 210px; text-align: left;
                  background: var(--paper); border: 1px solid var(--line);
                  border-radius: 5px; box-shadow: 0 6px 24px rgba(0,0,0,0.14); }
.addwhere-panel label { display: flex; gap: 6px; align-items: baseline;
                        font-size: 0.82rem; padding: 2px 0;
                        text-transform: none; letter-spacing: 0; }
.addwhere-panel .waiting { margin-left: auto; font-family: var(--mono);
                           font-size: 0.7rem; color: var(--ink-faint); }
.addwhere-panel .btn { margin-top: 8px; width: 100%; }

/* The catalog's "filed" quick ranges. Same shape as the board's week picker
   (`.boardweeks`) deliberately — it is the same interaction, a small set of
   named ranges with one of them current, and a second visual idiom for it would
   be a second thing to learn. */
.datequick { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin: -6px 0 12px; font-size: 0.84rem; }
.datequick a { padding: 2px 9px; border: 1px solid var(--line);
  border-radius: 999px; color: var(--ink-soft); text-decoration: none; }
.datequick a:hover { border-color: var(--ink-soft); color: var(--ink); }
.datequick a.on { background: var(--ink); border-color: var(--ink);
  color: var(--paper); }
.datequick a.clearlink { border-color: transparent; text-decoration: underline; }
/* The filing date on a catalog row. Smaller than the rest of the dim line: it is
   the least interesting fact about most products and the most interesting about
   the ones somebody added this week. */
.filedon { font-size: 0.78rem; }

/* The catalog row somebody has just added to. `:target` and not a class, so it
   costs no JS and no state: the redirect carries `#p-<id>` and the browser both
   scrolls here and marks it. This is the confirmation for an add made deep in a
   long list, where the banner at the top of the page is not visible at all. */
#catalog-rows tr:target > td { background: var(--good-soft); }
#catalog-rows tr:target > td:first-child { box-shadow: inset 3px 0 0 var(--good); }

/* Where a product is bought, on its catalog row. A chip and not another dim
   line: it is the fact that turns a shopping list into an errand, so it reads
   at a glance rather than being the third grey sentence under a name. */
.storechip { display: inline-block; margin-top: 3px; padding: 1px 7px;
  border: 1px solid var(--line); border-radius: 4px; background: var(--paper);
  font-size: 0.76rem; color: var(--ink-soft); }
/* The store form in the photograph overlay. Wraps on a phone, where the select
   and the free-text box will not sit side by side. */
.catlb-store { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin: 10px 0 0; }
.catlb-store label { font-size: 0.82rem; color: var(--ink-soft); }
.catlb-store input[name="store_other"] { flex: 1 1 140px; min-width: 120px; }

/* ==========================================================================
   The groceries page, consolidated (Aug 2026): open orders as collapsible
   sections, a compact grid inside each, stepper quantities, typeahead add.
   ========================================================================== */

/* One open order, collapsed to a line. A <details> and not a tab: the page is
   every house at once now, and four grids stacked open would bury the second
   house's order below the first house's fold. */
details.openorder { border: 1px solid var(--line); border-radius: 8px;
  background: var(--paper-raised); margin-bottom: 10px; }
details.openorder > summary { display: flex; align-items: center; gap: 10px;
  padding: 10px 14px; cursor: pointer; list-style: none; }
details.openorder > summary::-webkit-details-marker { display: none; }
details.openorder > summary::after { content: "›"; margin-left: auto;
  color: var(--ink-faint); transform: rotate(90deg); transition: transform .12s; }
details.openorder[open] > summary::after { transform: rotate(-90deg); }
details.openorder > summary .housename { font-weight: 600; }
details.openorder > summary .started { color: var(--ink-faint);
  font-size: 0.82rem; }
details.openorder .orderbody { padding: 0 14px 12px; border-top: 1px solid
  var(--line-soft); }
details.openorder.noorder { background: transparent; }
details.openorder.noorder > summary { color: var(--ink-faint); }

/* THE COMPACT GRID. Rows tightened on purpose — a shopping list is scanned,
   not read, and the old table spent three lines per line. The reason column is
   gone from the markup (the row's title attribute still says why, so the fact
   survives at hover cost rather than column cost). */
table.ordergrid td { padding: 4px 8px; font-size: 0.9rem; }
table.ordergrid td.oname { line-height: 1.25; }
table.ordergrid .onote { display: block; font-size: 0.78rem; }

/* The quantity stepper. Minus and plus are real submit buttons carrying
   delta=-1/+1, so the stepper works with no script at all; the typed number
   saves through the same form. The Save button is the no-JS fallback ONLY —
   DOM-first so that Enter in the input submits it (the implicit-submission
   default is the first submit button in tree order, and without this the Enter
   key would press the minus), flex-ordered last so it renders at the end, and
   hidden entirely once the script is running, because a change-event submit
   makes it dead weight. */
form.qtystep { display: inline-flex; align-items: center; gap: 4px; }
form.qtystep .qtysave { order: 4; }
html:not(.no-js) form.qtystep .qtysave { display: none; }
form.qtystep button.step { width: 26px; height: 26px; padding: 0;
  border: 1px solid var(--line); border-radius: 6px; background:
  var(--paper-raised); color: var(--ink-soft); font-size: 0.95rem;
  line-height: 1; cursor: pointer; }
form.qtystep button.step:hover { border-color: var(--ink-soft);
  color: var(--ink); }
form.qtystep input[name="quantity"] { width: 52px; text-align: center;
  padding: 3px 4px; }
form.qtystep .unit { order: 3; }

/* Adding into an open order, inline, with the catalog behind a datalist. */
form.addinline { display: flex; align-items: center; gap: 8px; margin-top: 10px;
  flex-wrap: wrap; }
form.addinline input[name="name"] { flex: 1 1 220px; min-width: 180px; }
form.addinline input[name="quantity"] { width: 64px; }

/* The restock and history disclosures share the open-order skin. */
details.grocdisc { border: 1px solid var(--line); border-radius: 8px;
  background: var(--paper-raised); margin-bottom: 10px; }
details.grocdisc > summary { padding: 10px 14px; cursor: pointer;
  font-weight: 600; }
details.grocdisc > .discbody { padding: 0 14px 12px; }
details.grocdisc .househead { margin: 10px 0 4px; font-size: 0.82rem;
  text-transform: uppercase; letter-spacing: 0.04em; color: var(--ink-faint); }

/* ---------- the peek bubble ----------
   One element for the whole page, created by peek.js, absolutely positioned
   so nothing reflows under it. Reads like the row it repeats, slightly
   raised; max-width keeps a long task title from becoming a banner. */
.peek-bubble {
  position: absolute; z-index: 60; max-width: min(420px, 86vw);
  padding: 8px 12px; border-radius: 8px;
  background: var(--ink); color: var(--paper-raised);
  font-size: 0.85rem; line-height: 1.45;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);
  pointer-events: none; /* the bubble never eats the second tap */
  overflow-wrap: break-word;
}

/* =============================================================================
   Reference states, camera-first (069) — additive section, nothing above edited
   ============================================================================= */

/* The capture strip: thumbnails land here as each upload finishes. Fixed row
   height so the strip grows sideways, never pushes the form around. */
.capstrip {
  display: flex; gap: 8px; margin-top: 10px;
  overflow-x: auto; padding-bottom: 4px;
}
.capstrip img {
  height: 84px; width: auto; border-radius: 8px; flex: none;
  border: 1px solid var(--line); object-fit: cover;
  animation: capstrip-in .25s ease;
}
@keyframes capstrip-in { from { opacity: 0; transform: scale(.94); } }

/* The processing line. The pulse is the visual indicator that something is
   happening BEFORE the record exists — silence there reads as a hang. */
.capstatus { margin-top: 8px; font-size: .88rem; color: var(--ink-soft); }
.capstatus.busy::before {
  content: ""; display: inline-block; width: 9px; height: 9px;
  border-radius: 50%; margin-right: 8px; background: var(--accent);
  animation: capstatus-pulse 1.1s ease-in-out infinite;
}
@keyframes capstatus-pulse { 50% { opacity: .25; } }

/* A four-field place row (house / room / spot / storage). */
.row.four {
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px;
}
@media (max-width: 900px) { .row.four { grid-template-columns: 1fr 1fr; } }
@media (max-width: 540px) { .row.four { grid-template-columns: 1fr; } }

/* Its two- and three-field siblings. asset_new.html used both since birth
   and neither was ever defined — the field pairs rendered as plain stacked
   blocks at every width (the mobile plan's found-in-passing list). */
.row.two {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px;
}
.row.three {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px;
}
@media (max-width: 720px) { .row.three { grid-template-columns: 1fr 1fr; } }
@media (max-width: 540px) { .row.two, .row.three { grid-template-columns: 1fr; } }

/* ---------------------------------------------------------------------------
   The library: house → floor → room → tiles. Native <details> at every level,
   so the hierarchy works with no script at all.
   --------------------------------------------------------------------------- */
.refhouse {
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--paper-raised); box-shadow: var(--shadow);
  margin-top: 14px;
}
.refhouse > summary {
  display: flex; align-items: center; gap: 14px;
  padding: 16px 18px; cursor: pointer; list-style: none;
}
.refhouse > summary::-webkit-details-marker { display: none; }
.refhouse > summary::before {
  content: "▸"; color: var(--ink-faint); font-size: .85rem; flex: none;
  transition: transform .15s ease;
}
.refhouse[open] > summary::before { transform: rotate(90deg); }
.refhouse > summary:hover .refhouse-name { color: var(--accent); }
.refhouse-name { font-size: 1.05rem; font-weight: 600; }
.refhouse-count { color: var(--ink-soft); font-size: .85rem; }
.refhouse-add { margin-left: auto; }

.reffloor { border-top: 1px solid var(--line-soft); padding: 0 18px 6px; }
.reffloor > summary {
  display: flex; align-items: baseline; gap: 10px;
  padding: 12px 0; cursor: pointer; list-style: none;
  font-weight: 600; font-size: .93rem;
}
.reffloor > summary::-webkit-details-marker { display: none; }
.reffloor > summary::before { content: "▸"; color: var(--ink-faint); font-size: .8rem; }
.reffloor[open] > summary::before { content: "▾"; }
.reffloor > summary:hover { color: var(--accent); }

.refroom { padding: 2px 0 14px 18px; }
.refroom-name { margin: 0 0 8px; font-size: .88rem; color: var(--ink-soft); }

/* The tiles: a horizontal strip per room, each tile one reference state.
   Sideways scroll on purpose — a kitchen with six states stays one row tall,
   which is what keeps the hierarchy scannable. */
.reftiles { display: flex; gap: 12px; overflow-x: auto; padding-bottom: 6px; }
.reftile {
  flex: none; width: 190px; border: 1px solid var(--line);
  border-radius: 10px; overflow: hidden; background: var(--paper);
  color: inherit; transition: transform .12s ease, box-shadow .12s ease;
}
.reftile:hover { transform: translateY(-2px); box-shadow: var(--shadow); }
.reftile img { width: 100%; height: 110px; object-fit: cover; display: block; }
.reftile-blank {
  display: flex; align-items: center; justify-content: center;
  width: 100%; height: 110px; color: var(--ink-faint);
  background: var(--paper-sunken, var(--paper-raised));
}
.reftile-cap { display: block; padding: 9px 11px 11px; }
.reftile-cap strong { display: block; font-size: .86rem; line-height: 1.25; }
.reftile-cap em { display: block; font-style: normal; font-size: .78rem; color: var(--ink-soft); margin-top: 2px; }
.reftile-cap .dim { display: block; font-size: .74rem; margin-top: 4px; }

/* ---------------------------------------------------------------------------
   The task review: proposals as an editable, tickable list.
   --------------------------------------------------------------------------- */
.proposals { list-style: none; margin: 0; padding: 0; }
.proposal {
  display: flex; gap: 12px; align-items: flex-start;
  padding: 12px 0; border-bottom: 1px solid var(--line-soft);
}
.proposal:last-child { border-bottom: 0; }
.proposal-keep { padding-top: 9px; }
.proposal-keep input { width: 17px; height: 17px; accent-color: var(--accent); }
.proposal-body { flex: 1; display: flex; flex-direction: column; gap: 6px; }
.proposal-body input[name^="title_"] { font-weight: 600; }
/* An unticked row stays visible but reads as set aside, not deleted. */
.proposal:has(.proposal-keep input:not(:checked)) .proposal-body { opacity: .45; }

/* ==========================================================================
   Donations, reworked around the collection (071).
   ========================================================================== */

/* The call to action: a disclosure styled as an invitation rather than a
   form. Open by default only when nothing is collecting. */
details.startcollection { border: 1.5px dashed var(--line); border-radius: 10px;
  padding: 0; margin-bottom: 6px; background: var(--paper-raised); }
details.startcollection > summary { display: flex; align-items: baseline;
  gap: 10px; padding: 14px 16px; cursor: pointer; list-style: none;
  flex-wrap: wrap; }
details.startcollection > summary::-webkit-details-marker { display: none; }
details.startcollection .startcta { font-weight: 650; font-size: 1.05rem; }
details.startcollection[open] { border-style: solid; }
details.startcollection > form { margin: 0 12px 12px; }

/* A collection card: the place photo across the top, the facts below. The
   photo is the point — it is how somebody finds the bins. */
.collcard { padding: 0; overflow: hidden; display: flex; flex-direction: column; }
.collcard .collphoto { width: 100%; height: 130px; object-fit: cover;
  display: block; border-bottom: 1px solid var(--line-soft); }
.collcard .collbody { padding: 12px 14px; }

/* The collection page's header: photo beside the title, sized as a finding
   aid rather than a hero. */
.page-head.collhead { display: flex; gap: 14px; align-items: flex-start; }
.collhero img { width: 128px; height: 96px; object-fit: cover;
  border-radius: 10px; border: 1px solid var(--line); display: block; }

/* The add-by-search row. */
form.collsearch { display: flex; gap: 8px; align-items: center;
  margin-top: 12px; flex-wrap: wrap; }
form.collsearch input[type="search"] { flex: 1 1 240px; }
form.addtocoll { display: inline-flex; gap: 6px; align-items: center; }
form.addtocoll select { max-width: 140px; }

/* ---------------------------------------------------------------------------
   The vendors roster (vendors.html, table.vlist): dense on purpose. The
   default 12/16 cell padding reads generously on dossier pages; a roster
   sorted by last service is scanned, and the scan is worth two lines per
   vendor, not four. Same trade the rota (7/9) and the order grid (4/8)
   already made.
--------------------------------------------------------------------------- */
table.vlist tbody td { padding: 7px 12px; font-size: 0.92rem; }
table.vlist thead th { padding: 8px 12px; }
table.vlist .btn.compact { padding: 3px 10px; font-size: 0.8rem; }

/* The roster's listrow columns (Phase 2 — the vlist table's successor; the
   rules above outlive it only until the vendor dossier's own tables get
   their pass). Density kept: the roster trades two lines per vendor, same
   as vlist did. All minmax(0, …), law 3's rider. */
.lr-vendors {
  --lr-cols: minmax(0, 2.2fr) minmax(0, 1fr) minmax(0, 1.3fr)
             minmax(0, 1.7fr) minmax(0, 0.8fr);
}
.lr-vendors .listrow { padding: 7px 14px; font-size: 0.92rem; }
.lr-vendors .btn.compact { padding: 3px 10px; font-size: 0.8rem; }
/* The call button is a PHONE affordance: on a desktop the number in the
   Reach column reads better than a button beside every row. */
.lr-call { display: none; }
@media (max-width: 720px) {
  .lr-call { display: block; }
  .lr-call .callbtn {
    display: inline-flex; align-items: center; gap: 6px; width: auto;
  }
  .lr-call .callbtn svg {
    width: 15px; height: 15px; stroke: currentColor; fill: none;
    stroke-width: 2; stroke-linejoin: round;
  }
  /* The trailing dossier text is desktop reading; the kept line stays
     `category · date`. */
  .lr-vendors .lr-sub { display: none; }
}

/* =============================================================================
   Reference states, the walk continued — additive section, nothing above edited
   ============================================================================= */
/* The room header row: name left, "Start this as a task" right. The open
   disclosure drops its picker below without pushing the tiles sideways. */
.refroom-head { display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap; }
.refroom-head .refroom-name { margin: 0 0 8px; }
.refroom-start { margin-left: auto; font-size: .84rem; }
.refroom-start .rowform { margin: 6px 0 10px; }

/* The greyed "Start as a task" with its current-active-task note beneath —
   additive, reference-states walk continued. */
.startline { display: inline-flex; flex-direction: column; gap: 4px; }
.startline-note { font-size: .8rem; }

/* ---------- groceries: the summary line sits left (owner, 20 Aug) ----------
   `.ordercount` was written for the house TABS, where `margin: 0 auto` centers
   the badge over the house name. The open-order summaries reuse the class
   inside a flex row, where those same auto margins absorb the row's free space
   and shove the badge — and everything after it — toward the middle. Scoped
   re-zero rather than a change to `.ordercount` itself, which the tabs still
   need. Grocery-page selectors only; nothing the office renders uses them. */
details.openorder > summary .ordercount { margin: 0; display: inline-block; }

/* ---------- groceries: hover preview on an order line ----------
   The photograph and the packaging's description, on mouse-over of the name —
   "is this the one I meant" without leaving the order. Pure CSS: the grids are
   plain forms that work with no script, and this keeps that true. :focus-within
   so a keyboard reaches it too (the wrap is tabindex="0"). The image inside is
   loading="lazy" in the markup, so fifty hidden cards cost zero fetches until
   one is shown. */
.hoverwrap { border-bottom: 1px dotted var(--line); cursor: default; }
/* position: FIXED, coordinates set by the small script on each grocery grid
   page — the grids live in .table-wrap (overflow-x: auto), and an absolutely
   positioned card inside a scroll container is clipped at the table's edge,
   which is exactly what the first version of this did. The card only shows
   once the script has .armed the wrap, so no-script means no half-clipped
   card — the catalog overlay remains the full answer there. */
.hovercard { display: none; position: fixed; z-index: 30;
  width: max-content; max-width: 320px; padding: 10px 12px;
  border: 1px solid var(--line); border-radius: 8px;
  background: var(--paper-raised); box-shadow: 0 6px 24px rgba(0,0,0,.18);
  font-size: 0.82rem; line-height: 1.35; }
.hoverwrap.armed:hover .hovercard, .hoverwrap.armed:focus-within .hovercard { display: flex; gap: 10px; }
.hovercard img { width: 84px; height: 84px; object-fit: contain; flex: none;
  border-radius: 4px; background: var(--paper); }
.hovercard .hovertext { display: block; min-width: 0; }
.hovercard .hovertext strong { display: block; margin-bottom: 2px; }
.hovercard .hovertext .dim { display: block; }
.hovercard .hovernotes { display: block; margin-top: 6px; color: var(--ink-soft); }

/* ---------- groceries: the catalog name opens the overlay ----------
   The same claim the thumbnail makes, on the word somebody actually reads.
   Quiet underline, solid on hover — a catalog of five hundred blue links would
   drown the page. */
a.cat-name { color: inherit; text-decoration: underline;
  text-decoration-style: dotted; text-decoration-color: var(--ink-faint);
  text-underline-offset: 2px; }
a.cat-name:hover { text-decoration-style: solid; }

/* ---------- groceries: notes on a product, in the overlay ---------- */
.catlb-usernotes { margin: 12px 0; }
.catlb-usernotes h4 { margin: 0 0 6px; font-size: 0.86rem; color: var(--paper); }
.catlb-usernotes .pnote { padding: 8px 10px; margin-bottom: 6px; border-radius: 4px;
  background: color-mix(in srgb, var(--paper) 10%, transparent);
  border-left: 3px solid color-mix(in srgb, var(--paper) 35%, transparent); }
.catlb-usernotes .pnote-body { margin: 0; font-size: 0.84rem; color: var(--paper);
  white-space: pre-wrap; }
.catlb-usernotes .pnote-by { margin: 4px 0 0; font-size: 0.74rem;
  color: color-mix(in srgb, var(--paper) 55%, transparent); }
.catlb-note { display: flex; gap: 8px; align-items: flex-start; margin-top: 8px; }
.catlb-note textarea { flex: 1 1 auto; min-width: 0; font-size: 0.84rem; }
/* --- Enrollment (staff wizard) ------------------------------------------ */
/* New classes only — nothing existing is restyled. The office shares this
   file (its own digest covers it), so additions keep to their own names. */
.enroll-steps { display:flex; align-items:center; gap:.5rem; flex-wrap:wrap;
  margin:0 0 1.1rem; font-size:.85rem; color:var(--muted, #667); }
.enroll-step { display:inline-flex; align-items:center; gap:.4rem;
  padding:.25rem .6rem; border:1px solid var(--line, #dcdfe4);
  border-radius:999px; background:var(--card, #fff); }
.enroll-step em { font-style:normal; font-weight:700; opacity:.6; }
.enroll-step.current { border-color:var(--accent, #2f6f4f);
  color:var(--accent, #2f6f4f); font-weight:600; }
.enroll-step.done { opacity:.65; }
.enroll-sep { opacity:.4; }

.idcard { border:1px solid var(--line, #dcdfe4); border-radius:10px;
  margin:0 0 .9rem; overflow:hidden; }
.idcard-head { padding:.45rem .8rem; font-weight:600; font-size:.8rem;
  text-transform:uppercase; letter-spacing:.04em;
  background:var(--wash, #f5f6f7); border-bottom:1px solid var(--line, #dcdfe4); }
.idcard-body { padding:.8rem; }
.idcard-body .pill { margin-left:.5rem; }

.arealist { display:grid; grid-template-columns:repeat(auto-fill, minmax(220px, 1fr));
  gap:.35rem .9rem; }
.arearow { display:flex; align-items:baseline; gap:.45rem; }
.areamark { font-size:.78rem; }

/* --- Folding sections (staff detail) ------------------------------------- */
/* The chevron is the affordance the sections lacked: Contact was collapsible
   and said so only in prose, Employment was not collapsible at all. One class
   so the three sections cannot drift. New selectors only. */
details.fold > summary { cursor: pointer; list-style: none; }
details.fold > summary::-webkit-details-marker { display: none; }
details.fold > summary h2::after {
  content: "›"; display: inline-block; margin-left: 10px;
  color: var(--muted, #667); font-weight: 400;
  transition: transform .15s ease; transform: rotate(90deg);
}
details.fold:not([open]) > summary h2::after { transform: rotate(0deg); }
details.fold > summary:hover h2 { color: var(--accent, #2f6f4f); }

/* =============================================================================
   The property page as a table of contents (four closed doors) — additive
   ============================================================================= */
.propsection {
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--paper-raised); box-shadow: var(--shadow);
  margin-top: 14px;
}
.propsection > summary {
  display: flex; align-items: baseline; gap: 12px;
  padding: 15px 18px; cursor: pointer; list-style: none;
}
.propsection > summary::-webkit-details-marker { display: none; }
.propsection > summary::before {
  content: "▸"; color: var(--ink-faint); font-size: .85rem; flex: none;
  transition: transform .15s ease;
}
.propsection[open] > summary::before { transform: rotate(90deg); }
.propsection > summary:hover .propsection-name { color: var(--accent); }
.propsection-name { font-size: 1.02rem; font-weight: 600; }
.propsection-count { color: var(--ink-soft); font-size: .85rem; }
.propsection-body { padding: 2px 18px 16px; }

.floorgroup { border-top: 1px solid var(--line-soft); }
.floorgroup > summary {
  display: flex; align-items: baseline; gap: 10px;
  padding: 11px 0; cursor: pointer; list-style: none;
  font-weight: 600; font-size: .92rem;
}
.floorgroup > summary::-webkit-details-marker { display: none; }
.floorgroup > summary::before { content: "▸"; color: var(--ink-faint); font-size: .8rem; }
.floorgroup[open] > summary::before { content: "▾"; }
.floorgroup > summary:hover { color: var(--accent); }

/* A room as one row: name, activity pills, the dim facts pushed right. */
.roomrows li { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.roomrow-activity { display: inline-flex; gap: 6px; }
.roomrow-activity .pill { font-size: .72rem; }
.roomrow-facts { margin-left: auto; font-size: .78rem; }

/* A button for the one action on a page that hides a record. Warn tones, not
   red-alert: it is reversible and says so beside it. New selector only. */
.btn.warn { background: var(--warn-soft); color: var(--warn); border-color: var(--warn); }

/* ==========================================================================
   The For-you feed — the home page's one queue (Aug 2026). Four cards
   ("Your queue", "Needs you", "While you were away", "Coming up") became one
   card with three labelled groups; these are the group labels' clothes.
   ========================================================================== */
.c-foryou .fygroup { display: flex; align-items: baseline; gap: 8px;
  margin: 14px 0 4px; font-size: 0.72rem; text-transform: uppercase;
  letter-spacing: 0.08em; color: var(--ink-faint); font-weight: 600; }
.c-foryou .fygroup:first-of-type { margin-top: 2px; }
.c-foryou .fycount { text-transform: none; letter-spacing: 0;
  font-weight: 400; color: var(--ink-faint); }
.c-foryou .fymore { margin-left: auto; text-transform: none; letter-spacing: 0;
  font-weight: 400; font-size: 0.78rem; color: var(--ink-faint); }
.c-foryou .fymore:hover { color: var(--ink); }
/* The news rows ride the shared feed include; inside this card they sit
   flush with the groups rather than in their own box. */
.c-foryou .feed { margin-top: 2px; }

/* =============================================================================
   Inventory home: the module doors — additive section
   ============================================================================= */
.modrow { display: flex; gap: 10px; flex-wrap: wrap; margin: 4px 0 18px; }
.modlink {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 9px 16px; border: 1px solid var(--line); border-radius: 10px;
  background: var(--paper-raised); font-size: .9rem; font-weight: 600;
  color: inherit; transition: transform .12s ease, box-shadow .12s ease;
}
.modlink:hover { transform: translateY(-1px); box-shadow: var(--shadow); color: var(--accent); }
.modlink svg { width: 16px; height: 16px; }

/* The search as the hero: the register's one full-width control. */
.invsearch { align-items: flex-end; }
.invsearch .f.grow input[type="search"] { font-size: 1rem; padding: 11px 14px; }

/* Phase 4's detail-page hoist: on a phone, a facts-first dossier puts its
   sidebar ABOVE the media column — the vendor page's facts hold THE PHONE
   NUMBER and the routine's hold the Run form, and the single-column collapse
   was burying each under four tables of history. detail.html (the asset)
   deliberately does NOT carry the class: there the photograph is the
   identity and leads. */
@media (max-width: 860px) {
  .detail.facts-first { display: flex; flex-direction: column; }
  .detail.facts-first .detail-facts { order: -1; }
}

/* Phase 6, the desks' chrome discipline: on a desktop, a register's column
   labels stay put while its rows scroll — the reader half of "the window is
   the frame". 62px is the sticky masthead's height; the labels tuck under
   it. Phones never see .lr-head at all. */
@media (min-width: 721px) {
  .lr-head {
    position: sticky; top: 62px; z-index: 4;
    background: var(--paper-raised);
  }
  .schedweek thead th {
    position: sticky; top: 62px; z-index: 4;
    background: var(--paper);
  }
}

/* Phase 6, the fluid wrap: the two widest working surfaces earn width past
   the 1180px cap on a big window — the board's fortnight and the home grid
   are exactly the screens whose columns starve at laptop width. :has-scoped
   so no template knows about it; prose pages keep their measure. */
@media (min-width: 1280px) {
  body:has(.home-grid) .wrap,
  body:has(.schedweek) .wrap { max-width: 1520px; }
}

/* The hours pages (Phase 3): open entries, the variance report, the totals.
   Numeric columns whose headers ARE the meaning; the phone lines carry their
   own words instead. All tracks minmax(0, …), law 3's rider. */
.lr-hours {
  --lr-cols: minmax(0, 1.4fr) minmax(0, 1.3fr) minmax(0, 1fr)
             minmax(0, 1fr) minmax(0, 0.4fr);
}
.lr-variance {
  --lr-cols: minmax(0, 1.4fr) minmax(0, 1.2fr) minmax(0, 1.2fr)
             minmax(0, 0.9fr) minmax(0, 0.4fr);
}
.lr-hourstotals {
  --lr-cols: minmax(0, 1.5fr) minmax(0, 1.1fr) minmax(0, 0.9fr)
             minmax(0, 1.1fr) minmax(0, 0.9fr);
}
.lr-hours .listrow.flagged { background: var(--warn-soft); }
@media (max-width: 720px) {
  .lr-hourstotals .lr-sub { display: none; }
}

/* "Settle this" — a correction form folded inside its row, both
   presentations: the fold spans the row's full width below the cells. */
.lr-more { grid-column: 1 / -1; font-size: 0.82rem; }
.lr-more > summary { cursor: pointer; color: var(--ink-faint); }
.lr-more > summary:hover { color: var(--accent); }
.lr-more[open] { padding-top: 8px; }

/* Bill Pay's two rows (Phase 3): the bills strip and the account register.
   Money data was the worst destack casualty — an amount input two unlabeled
   blocks below its payee. All tracks minmax(0, …), law 3's rider. */
.lr-bills {
  --lr-cols: minmax(0, 1.8fr) minmax(0, 1fr) minmax(0, 0.9fr)
             minmax(0, 0.9fr) minmax(0, 0.9fr) minmax(0, 1.1fr);
}
.lr-accounts {
  --lr-cols: minmax(0, 2fr) minmax(0, 1fr) minmax(0, 0.9fr)
             minmax(0, 1.6fr) minmax(0, 0.9fr);
}
/* Mark paid keeps the row's right edge on both presentations — it is the
   page's one action (law 11) — and its optional amount field must not
   balloon to the .wrap select rule's full row. */
.lr-bills .lr-side { justify-self: end; }
@media (max-width: 720px) {
  .lr-bills .payform { display: flex; flex-direction: column; gap: 6px; align-items: flex-end; }
  .lr-bills .payform input[name="amount"] { min-height: var(--tap); }
}

/* The house month's two presentations (mobile plan, Phase 3): the grid is
   desktop's, the agenda is the phone's — same weeks structure, so they
   cannot disagree. The agenda compresses the empty days to one honest line
   rather than forty blank cells nobody scrolls a phone for. */
.month-agenda { display: none; }
@media (max-width: 720px) {
  .monthgrid { display: none; }
  .month-agenda { display: block; list-style: none; margin: 0; padding: 0; }
  .ma-day {
    display: grid; grid-template-columns: 64px minmax(0, 1fr);
    gap: 2px 10px; padding: 8px 2px;
    border-bottom: 1px solid var(--line-soft);
  }
  .ma-day.today .ma-date { color: var(--accent); font-weight: 600; }
  .ma-date { font-size: 0.82rem; font-weight: 500; white-space: nowrap; }
  .ma-items { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
  .ma-item { font-size: 0.82rem; line-height: 1.4; overflow-wrap: anywhere; }
  .ma-quiet { padding: 8px 2px; font-size: 0.78rem; color: var(--ink-faint); }
}

/* The register's listrow columns. components.css owns the row's machinery
   and consumes this custom property, so this line carries no specificity and
   the phone collapse always wins. Every track minmax(0, …) — law 3's rider:
   a bare 1fr's min-content floor is the longest serial number. */
.lr-inventory {
  --lr-cols: 44px minmax(0, 2fr) minmax(0, 1fr) minmax(0, 1fr)
             minmax(0, 1.3fr) minmax(0, 1fr) minmax(0, 0.9fr);
}
/* Review mode (f60498d8) adds a tick as a real grid child and puts the
   queue's two verbs beside the capture date — so the row needs a LEADING
   TRACK and a wider tail. Without this the checkbox was an eighth child on
   the seven-track grid above: every cell shifted one column right and the
   date-and-verbs cell wrapped into the 44px thumb track, one letter per
   line (the owner's 31 Aug screenshot). */
.lr-inventory.lr-review {
  --lr-cols: 22px 44px minmax(0, 1.6fr) minmax(0, 0.9fr) minmax(0, 0.9fr)
             minmax(0, 1.1fr) minmax(0, 0.9fr) minmax(0, 1.7fr);
}
/* Date and verbs flow on one compact line and wrap as a pair of buttons,
   never mid-word: .lr-col's overflow-wrap:anywhere is for serial numbers,
   not for button labels. */
.lr-review .lr-when { display: flex; flex-wrap: wrap; gap: 4px 10px; align-items: center; }
.lr-review .lr-verbs { display: flex; gap: 6px; }
.lr-review .lr-verbs .btn { white-space: nowrap; }
@media (max-width: 720px) {
  /* components.css's phone collapse knows three tracks (thumb, body, side)
     and hides every .lr-col that isn't lr-keep — which would leave the
     review queue with no verbs at all on the phones it was built for. Four
     tracks here, and the date-and-verbs cell stays, as its own line. */
  .lr-inventory.lr-review .listrow { grid-template-columns: 22px 44px minmax(0, 1fr); }
  .lr-inventory.lr-review .lr-col.lr-when { display: flex; margin-top: 4px; }
}

/* ---------- storage: Where Things Live (083) ----------
   Places as tiles with cover photographs and recursive counts; the place
   page's two-column layout, photo strip and story fold. Storage-page
   selectors only; nothing the office renders uses them. */
.place-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px; }
.place-tile { display: block; background: var(--paper-raised); border: 1px solid var(--line);
  border-radius: var(--radius); overflow: hidden; transition: border-color .12s; }
.place-tile:hover { border-color: var(--accent); }
.place-cover { display: block; height: 88px; position: relative; background: var(--line-soft); }
.place-cover img { width: 100%; height: 100%; object-fit: cover; display: block; }
.place-cover .pill { position: absolute; top: 8px; left: 8px;
  background: color-mix(in srgb, var(--paper-raised) 88%, transparent); }
/* Kind-tinted grounds so a photo-less tile still reads as a place, not a gap.
   Warm neutrals off the palette; replaced by the photograph the moment one
   exists. */
.place-cover.k-closet, .place-cover.k-cabinet { background: linear-gradient(160deg, #e2e7dc, #cfd8c8); }
.place-cover.k-attic, .place-cover.k-basement, .place-cover.k-cellar,
.place-cover.k-garage { background: linear-gradient(160deg, #e7dfd2, #d9cfc0); }
.place-cover.k-bin, .place-cover.k-shelf, .place-cover.k-drawer,
.place-cover.k-rack { background: linear-gradient(160deg, #e9e2d4, #dcd2bf); }
.place-cover.k-unit, .place-cover.k-offsite, .place-cover.k-vault { background: linear-gradient(160deg, #ded9d2, #ccc5bb); }
.place-body { display: block; padding: 10px 12px 12px; }
.place-body b { font-size: .92rem; font-weight: 600; display: block; }
.place-body .line2 { display: block; font-size: .76rem; margin-top: 2px; }
.place-body .line3 { display: flex; gap: 8px; align-items: center; font-size: .76rem; margin-top: 7px; flex-wrap: wrap; }

.place-twocol { display: grid; grid-template-columns: 1.8fr 1fr; gap: 22px; align-items: start; }
@media (max-width: 760px) { .place-twocol { grid-template-columns: 1fr; } }
.place-hero { display: flex; gap: 10px; margin-bottom: 18px; flex-wrap: wrap; }
.place-hero .shot { display: block; width: 150px; height: 104px; border-radius: 9px;
  overflow: hidden; border: 1px solid var(--line); background: var(--line-soft); }
.place-hero .shot img { width: 100%; height: 100%; object-fit: cover; display: block; }
.place-actions { display: flex; gap: 12px; align-items: center; margin-top: 12px; }
.place-story { font-size: .8rem; line-height: 1.8; }
.placepath { font-size: .84rem; color: var(--ink-soft); }
.factlist { font-size: .84rem; }
.factlist dt { float: left; clear: left; width: 84px; color: var(--ink-faint); padding: 4px 0; }
.factlist dd { margin: 0 0 0 92px; padding: 4px 0; border-bottom: 1px solid var(--line-soft); }
.mono-sm { font-family: var(--mono); font-size: .78rem; }

/* =============================================================================
   The item page edits itself — additive section
   ============================================================================= */
/* A fact that can be tapped: the value is the summary, the pencil appears on
   hover/focus so the affordance is discoverable without shouting. */
.factedit > summary { cursor: pointer; list-style: none; display: inline; }
.factedit > summary::-webkit-details-marker { display: none; }
.factedit-pen {
  margin-left: 7px; font-size: .78rem; color: var(--ink-faint);
  opacity: 0; transition: opacity .12s ease;
}
.fact:hover .factedit-pen, .factedit > summary:focus .factedit-pen { opacity: 1; }
.factedit[open] .factedit-pen { opacity: 0; }
.factedit[open] > summary { display: none; } /* the form replaces the value */
.factedit form { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-start; margin-top: 2px; }
.factedit input, .factedit textarea, .factedit select { flex: 1; min-width: 160px; }

/* The paperwork fold: a whole section behind a details, its head the summary. */
details.section.fold > summary { cursor: pointer; list-style: none; }
details.section.fold > summary::-webkit-details-marker { display: none; }
details.section.fold > summary .section-head h2::after {
  content: "▸"; margin-left: 9px; font-size: .8rem; color: var(--ink-faint);
  font-weight: 400; display: inline-block;
}
details.section.fold[open] > summary .section-head h2::after { content: "▾"; }
details.section.fold > summary:hover h2 { color: var(--accent); }

/* ---------- storage: the wizard's front door ----------
   The one oversized call to action in the module — "New storage location" on
   the index head, and the small step marker the wizard pages wear.
   Storage-page selectors only. */
.btn.big { padding: 11px 22px; font-size: 0.98rem; border-radius: 10px;
  align-self: center; white-space: nowrap; }
.wizstep { display: inline-block; padding: 1px 10px; border-radius: 20px;
  background: var(--accent-soft); color: var(--accent); font-size: 0.76rem;
  font-weight: 600; letter-spacing: .04em; margin-right: 4px; }

/* ---------- "New place here" (_place_new.html) ----------
   The inline place sheet, used by the capture draft and both item forms. It
   deliberately does not look like the page's other cards: it is a thing that
   opens inside a form somebody is already filling in, so it reads as a fold
   rather than a section. HouseOps-only in fact — the office renders no
   storage. */
.placenew { border: 1px dashed var(--line); border-radius: var(--radius);
  padding: 8px 12px; background: var(--paper); margin-top: 8px; }
.placenew > summary { cursor: pointer; font-size: 0.88rem; color: var(--accent);
  font-weight: 500; list-style: none; display: flex; align-items: center; gap: 7px; }
.placenew > summary::-webkit-details-marker { display: none; }
.pn-plus { font-size: 1rem; line-height: 1; }
.placenew[open] > summary { margin-bottom: 10px; }
.pn-where { margin: 0 0 10px; font-size: 0.82rem; color: var(--ink-faint); }
.pn-row { display: flex; gap: 10px; flex-wrap: wrap; }
.pn-field { flex: 1 1 180px; display: flex; flex-direction: column; gap: 4px;
  font-size: 0.8rem; color: var(--ink-soft); }
.pn-field.pn-kind { flex: 0 1 140px; }
.pn-field input, .pn-field select {
  /* 16px so iOS does not zoom the page when the field takes focus — the
     capture screens learned this the hard way. */
  padding: 8px 10px; border: 1px solid var(--line); border-radius: 7px;
  background: var(--paper-raised); color: var(--ink); font-size: 16px; }
.pn-cover { display: block; margin: 10px 0; cursor: pointer; }
.pn-cover-face { display: flex; flex-direction: column; align-items: center;
  gap: 3px; padding: 14px; border: 1px dashed var(--line);
  border-radius: var(--radius); background: var(--paper-raised);
  text-align: center; }
.pn-cover-face svg { width: 26px; height: 26px; stroke: var(--ink-faint);
  fill: none; stroke-width: 1.8; }
.pn-cover-face b { font-size: 0.86rem; font-weight: 500; }
.pn-cover-face em { font-size: 0.76rem; color: var(--ink-faint); font-style: normal; }
.pn-cover-held img { width: 84px; height: 84px; object-fit: cover;
  border-radius: 8px; margin-top: 8px; border: 1px solid var(--line); }
.pn-form > .btn { margin-top: 4px; }

/* The place's photographs, with the face called out. The wrapper exists so a
   promote button can sit under a thumbnail without being inside the link. */
.shotwrap { display: flex; flex-direction: column; gap: 5px; align-items: center; }
.shot-face-tag { font-size: 0.7rem; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--accent); font-weight: 600; }
.shot-promote .btn { font-size: 0.72rem; padding: 3px 8px; }
.place-addshot { margin-top: 12px; max-width: 320px; }

/* ---------- a slow submit, saying so (submitting.js) ----------
   The owner pressed a confirm twice because four seconds of upload looked
   identical to nothing happening. The disable is the guard; this is the part
   that tells the truth. Indeterminate on purpose — the server reports no
   progress, and a bar that fills at an invented rate arrives at 90% and
   stops, which is worse than one that only says "still going". */
.working-note { display: flex; align-items: center; gap: 9px; margin: 8px 0 0;
  font-size: 0.84rem; color: var(--ink-soft); }
.working-bar { flex: 1 1 auto; max-width: 160px; height: 3px; border-radius: 2px;
  background: var(--line-soft); overflow: hidden; position: relative; }
.working-bar::after { content: ""; position: absolute; inset: 0 auto 0 0;
  width: 40%; border-radius: 2px; background: var(--accent);
  animation: working-slide 1.15s ease-in-out infinite; }
@keyframes working-slide {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(250%); }
}
.btn.is-working, button.is-working { opacity: 0.72; cursor: progress; }
@media (prefers-reduced-motion: reduce) {
  /* No bar is inserted at all in this case (submitting.js checks), so there
     is nothing to stop animating — the sentence carries it. */
  .working-bar::after { animation: none; width: 100%; }
}

/* The pager — a new component, added rather than moved.

   Named as a coordination event per CLAUDE.md: this file is house ops' by the
   ownership list and shared in fact, since office/base.html loads it. Nothing
   existing was touched, so no office surface can shift; a `.pager` appears only
   where a template includes _pager.html, and no office template does. */
.pager {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; margin: 18px 0 4px; padding-top: 14px;
  border-top: 1px solid var(--line-soft);
}
.pager-at { color: var(--ink-faint); font-size: 0.9rem; }
@media (max-width: 520px) {
  /* Gap only. An earlier version moved the position to `order: 3` meaning to
     drop it onto its own line — but `.pager` has no flex-wrap, so instead of
     wrapping it swapped places with the button and put NEXT ON THE LEFT, which
     is where a Back would be. Seen in the browser at 375px, not in the rule. */
  .pager { gap: 8px; }
  .pager-at { font-size: 0.85rem; }
}

/* Tap-to-edit rows (inline-edit.js). Additive: a new component, nothing moved.
   The closed state has to look like a value and behave like a button, because
   the whole point is that a person taps what they are reading. */
.tapedit { display: inline; }
.tapedit-show {
  font: inherit; color: inherit; background: none; border: 0; padding: 0;
  text-align: left; cursor: pointer;
  border-bottom: 1px dashed var(--line);
}
.tapedit-show:hover { border-bottom-color: var(--ink-faint); }
.tapedit-fields { display: inline-flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.tapedit-fields input { font: inherit; padding: 4px 8px; max-width: 16em; }
.tapedit-fields .hint { flex-basis: 100%; }


/* ── The name menu on phones ───────────────────────────────────────────
   The who-name's dropdown (the learning doors) lives OUTSIDE the hamburger
   panel, so the panel's "navmenu always open" rule reached it and rendered
   the whole menu permanently expanded under the header on every phone page
   — found in the tour pipeline's own screenshots, which photograph what a
   phone actually sees. On small screens the name stays a plain link to /me
   and the menu hides; every door in it remains one tap away in the ? bubble
   on every screen, which is the promise the offer card already makes. */
@media (max-width: 720px) {
  .who .navgroup .navmenu { display: none; }
  .who .navgroup:hover .navmenu,
  .who .navgroup:focus-within .navmenu { display: none; }
  .who .navgroup .caret { display: none; }
}


/* The agent-activity page's listrow columns (Phase 2 — the two tables'
   successors; this page destacked into 5 unlabeled lines per action, the
   mono tool name indistinguishable from the date). Two lists, two templates,
   because the pending questions and the ledger never shared a shape. All
   minmax(0, …), law 3's rider — the tool names are exactly the unbreakable
   tokens (`create_asset`) the rider exists for. */
.lr-questions {
  --lr-cols: minmax(0, 2.4fr) minmax(0, 1fr) minmax(0, 0.8fr);
}
.lr-ledger {
  --lr-cols: minmax(0, 1.3fr) minmax(0, 2fr) minmax(0, 1fr) minmax(0, 0.8fr);
}
@media (max-width: 720px) {
  /* The detail keeps its rowlink from the table days, and rowlink is a
     block — inside the kept line that would break the dot-join, so on a
     phone it reads inline like the cells around it. */
  .lr-ledger .lr-col .rowlink { display: inline; }
}

/* The person's own page (/me): four short tables become four lists — next
   shifts, time off, documents, recent worked days. Date- or name-first rows;
   a phone keeps the shift's hours·where, the request's dates·status pill,
   the document's kind·date and the day's hours·where, deferring the clock's
   started/finished pair to the desk. All minmax(0, …), law 3's rider — a
   property name or a decision note is one long token away from sideways
   scroll. Same shapes serve the staff-preview branch, whose sample rows
   render through these very lists. */
.lr-meshifts {
  --lr-cols: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1.6fr);
}
.lr-metimeoff {
  --lr-cols: minmax(0, 1fr) minmax(0, 1.2fr) minmax(0, 1.8fr);
}
.lr-medocs {
  --lr-cols: minmax(0, 1.6fr) minmax(0, 1fr) minmax(0, 1.2fr);
}
.lr-medays {
  --lr-cols: minmax(0, 1.1fr) minmax(0, 1fr) minmax(0, 1.3fr)
             minmax(0, 0.7fr) minmax(0, 1.2fr);
}

/* A choice row: a label that IS a sentence with a radio or checkbox inside
   it, rather than a caption over a control. The bare-element `input` rule
   above stretches any control to width:100%, which detaches a nested radio
   from its own text and floats the circle mid-row — found by the owner on
   the feedback form's "what is this about?" radios, the first bare radio
   inside a .f anywhere in the app. The class undoes the three field
   assumptions at once: full width, uppercase caption, faint ink. */
.f label.choice {
  display: flex;
  align-items: center;
  gap: 8px;
  text-transform: none;
  letter-spacing: 0;
  font-size: 0.95rem;
  color: var(--ink);
}
.f label.choice input { width: auto; margin: 0; }

/* ---------------------------------------------------------------------------
   Standard weeks: master–detail (the owner's redesign, 26 Aug 2026).
   Names in a quiet rail, the chosen week and its editor on the right —
   replacing per-person expand/collapse sections. Selection is the URL, so
   the phone story is CSS only: with a selection the rail hides and the
   backlink leads home; without one the detail hides. Shared-file note: this
   block is additive and everything is .weeks- scoped.
--------------------------------------------------------------------------- */
.weeks-split { display: grid; grid-template-columns: minmax(220px, 300px) 1fr;
               gap: 22px; align-items: start; }
.weeks-rail { border: 1px solid #efeae2; border-radius: 10px; background: #fff;
              overflow: hidden; }
.weeks-filter { padding: 10px 10px 6px; display: flex; gap: 6px; }
.weeks-filter input { flex: 1; min-width: 0; }
.weeks-person { display: block; padding: 10px 14px; text-decoration: none;
                border-top: 1px solid #f4f0ea; color: inherit; }
.weeks-person:hover { background: #faf8f5; }
.weeks-person.sel { background: #f2ede5; box-shadow: inset 3px 0 0 #2c5f8a; }
.wp-name { display: block; font-weight: 600; }
.wp-meta { display: block; font-size: 0.8rem; color: #6b6459; margin-top: 1px; }
.wp-flag { color: #a05a12; font-weight: 600; }
.weeks-detail { min-width: 0; }
.weeks-back { display: none; margin-bottom: 8px; }
.weeks-days { list-style: none; margin: 0 0 14px; padding: 0;
              border: 1px solid #efeae2; border-radius: 10px; background: #fff;
              overflow: hidden; }
.weeks-days li { display: flex; gap: 14px; align-items: baseline;
                 padding: 8px 14px; border-top: 1px solid #f4f0ea; }
.weeks-days li:first-child { border-top: 0; }
.weeks-days li.off { background: #faf8f5; }
.wd-day { width: 44px; font-weight: 600; font-variant-numeric: tabular-nums; }
.wd-hours { font-variant-numeric: tabular-nums; }
.weeks-edit summary { cursor: pointer; font-weight: 600; }
.weeks-none { border: 1px dashed #e3dccf; border-radius: 10px; padding: 36px;
              text-align: center; color: #6b6459; }
.weeks-none .big { font-size: 1.15rem; color: #1c1a17; margin: 0 0 6px; }
@media (max-width: 720px) {
  .weeks-split { display: block; }
  .weeks-split.has-sel .weeks-rail { display: none; }
  .weeks-split:not(.has-sel) .weeks-detail { display: none; }
  .weeks-split.has-sel .weeks-back { display: inline-block; }
}

/* ---- Saved task views + Team workload (166 / 4b199ef0 / 1177d042) ----
   Additive block only: new class names, no existing selector touched — the
   app.css coordination rule's cheap end, named in the commit that adds it. */
.viewshelf { display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
             margin: 6px 0 14px; }
.viewpill  { display: inline-flex; flex-wrap: wrap; align-items: center;
             gap: 6px; padding: 4px 10px; border: 1px solid var(--line);
             border-radius: 999px; font-size: 0.9rem; }
.viewpill.active { border-color: var(--accent); background: var(--accent-soft, rgba(31,111,235,.08)); }
.viewpill form.inline { display: inline-flex; gap: 4px; flex-wrap: wrap; }
.viewsave summary { list-style: none; cursor: pointer; }
.viewsave summary::-webkit-details-marker { display: none; }
.viewsave form.inline { display: inline-flex; gap: 6px; flex-wrap: wrap;
                        align-items: center; margin-top: 6px; }
.viewsave input[name="name"] { min-width: 160px; }
.filters label.check { display: inline-flex; gap: 6px; align-items: center;
                       font-size: 0.9rem; white-space: nowrap; }
.workload-block { margin-bottom: 14px; }
.workload-block ul.clean { margin: 0; padding: 0; list-style: none; }
.workload-block ul.clean li { padding: 3px 0; }
.workload-rest-head { text-transform: uppercase; font-size: 0.72rem;
                      letter-spacing: 0.04em; margin-top: 6px; }

/* ---------- the task-page filter fold ----------
   Tasks' search form behind one bar on a phone (the page's script closes it
   there; markup ships open, so no JS means the old always-visible form).
   The summary is chrome for small screens only — desktop hides it and the
   form renders exactly as it always has. NAMED taskfold because components.css
   owns "filterfold" — a dormant :target contract whose field-hiding rule
   armed itself the day this block borrowed the name. */
.taskfold > summary { display: none; }
@media (max-width: 720px) {
  .taskfold > summary {
    display: flex; align-items: center; gap: 8px;
    padding: 10px 14px; margin-bottom: 12px;
    background: var(--paper-raised); border: 1px solid var(--line);
    border-radius: var(--radius); box-shadow: var(--shadow);
    font-size: 0.9rem; cursor: pointer; list-style: none;
  }
  .taskfold > summary::-webkit-details-marker { display: none; }
  .taskfold > summary::after {
    content: "\25BE"; margin-left: auto; color: var(--ink-faint);
    transition: transform 0.15s ease;
  }
  .taskfold[open] > summary::after { transform: rotate(180deg); }
}

/* ---------- priority pills ----------
   P1 burns, P2 warms, P3 is the quiet default, P4/P5 recede. Monospace
   digits so a column of them reads as a column. */
.pill.prio { font-family: var(--mono, ui-monospace, monospace); font-weight: 600; }
.pill.prio-1 { background: #b3261e; border-color: #b3261e; color: #fff; }
.pill.prio-2 { background: #b3261e14; border-color: #b3261e55; color: #8c1d17; }
.pill.prio-3 { color: var(--ink-soft); }
.pill.prio-4, .pill.prio-5 { color: var(--ink-faint); border-style: dashed; }

/* ---------------------------------------------------------------------------
   Video in the media surfaces (29 Aug 2026, the owner's ask). ADDITIVE ONLY —
   new selectors and a <video> sibling for existing ones; nothing above this
   block changes. Announced as a coordination event per the shared-file rule.
--------------------------------------------------------------------------- */
.gallery-focus video { display:block; width:100%; max-height:70vh; background:#000;
  border-radius:inherit; }
.gallery-thumb video { display:block; width:100%; height:100%; object-fit:cover;
  pointer-events:none; background:#000; }
.gallery-tile { position:relative; }
.gallery-playbadge { position:absolute; inset:auto auto 6px 6px; z-index:2;
  background:rgba(0,0,0,.62); color:#fff; border-radius:999px; font-size:10px;
  line-height:1; padding:5px 7px 5px 8px; pointer-events:none; }
.shotcell { position:relative; }
.shotcell video { width:100%; height:100%; object-fit:cover; display:block;
  background:#000; }
.shotplay { position:absolute; left:6px; bottom:6px; z-index:2;
  background:rgba(0,0,0,.62); color:#fff; border-radius:999px; font-size:10px;
  line-height:1; padding:5px 7px 5px 8px; pointer-events:none; }

/* Upload progress overlay (upload-progress.js) — same additive-only contract
   as the video block above it. */
.upload-overlay { position:fixed; inset:0; z-index:1000; display:grid; place-items:center;
  background:rgba(15,14,12,.55); backdrop-filter:blur(2px); }
.upload-card { background:var(--panel, #fff); color:inherit; border-radius:14px;
  padding:22px 26px; width:min(420px, 88vw); box-shadow:0 30px 60px -30px rgba(0,0,0,.6); }
.upload-title { font-weight:600; margin:0 0 12px; }
.upload-track { height:10px; border-radius:999px; background:rgba(127,127,127,.25);
  overflow:hidden; }
.upload-fill { height:100%; width:0%; border-radius:inherit;
  background:var(--accent, #2f6f4f); transition:width .25s ease; }
.upload-track.upload-processing .upload-fill { width:100% !important;
  animation:upload-pulse 1.2s ease-in-out infinite; }
@keyframes upload-pulse { 0%,100% { opacity:.45 } 50% { opacity:1 } }
.upload-detail { margin:10px 0 0; font-size:13px; opacity:.8; }
.upload-cancel { margin-top:14px; font-size:13px; background:none; border:1px solid
  rgba(127,127,127,.4); border-radius:999px; padding:7px 18px; cursor:pointer; }
@media (prefers-reduced-motion: reduce) {
  .upload-fill { transition:none } .upload-track.upload-processing .upload-fill { animation:none }
}

/* ---------- an item's location card ----------
   ADDITIVE ONLY: new class names, no existing selector touched, so nothing in
   the office (which loads this file for its typography and controls) moves.
   The card itself is .section + .facts, already here; what is new is the
   arrangement of a VALUE THAT STAYS A LINK beside a small editor, which the
   .factedit rows above cannot do — they put the value in the summary. */
.loccard { margin-bottom: 22px; }
.loccard .facts { margin-top: 10px; }
/* The floor, under the room's name rather than in a row of its own. */
.loc-floor { font-size: 0.76rem; margin-left: 8px; }
/* "change" as plain visible text, deliberately NOT the ghost pen the fact rows
   use: that pen is revealed by :hover, and the person this card is for is
   holding a phone in front of a cabinet. */
.locedit { display: inline; }
.locedit > summary {
  cursor: pointer;
  list-style: none;
  display: inline;
  margin-left: 8px;
  font-size: 0.74rem;
  color: var(--accent);
}
.locedit > summary::-webkit-details-marker { display: none; }
.locedit > summary:hover { text-decoration: underline; }
.locedit[open] > summary { display: none; }
.locedit form {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: flex-start;
  margin-top: 6px;
}
.locedit select { flex: 1 1 140px; min-width: 120px; }
/* The place picker is open rather than folded, so it is a row of its own. */
.locplace { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.locplace select { flex: 1 1 160px; min-width: 140px; }

/* The face of the place, on an item's Location card. ADDITIVE ONLY, and it
   deliberately RIDES `.place-cover` rather than restating it — the kind-tinted
   grounds and the image fill are that class's, so a new storage kind styled in
   one place is styled in both. These rules only resize it for a card. */
.locstore { display: flex; align-items: flex-start; gap: 10px; margin-bottom: 8px; }
.locshot {
  width: 76px;
  height: 58px;
  flex: 0 0 auto;
  border: 1px solid var(--line);
  border-radius: 8px;
  overflow: hidden;
}
.locshot-take { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; }
/* The no-script save. The file input submits on choose, so with script this is
   a second button for a job already done — the qtystep pattern, same reason. */
html:not(.no-js) .locshot-take .locshot-go { display: none; }
