/* Keystay design system — production stylesheet, no build step */

:root {
  --bg: #f6f5f2;
  --surface: #ffffff;
  --surface-2: #fbfaf8;
  --border: #e7e5df;
  --border-strong: #d6d3cb;
  --ink: #1c1b18;
  --ink-2: #5f5d57;
  --ink-3: #97948c;
  --accent: #1f7a63;
  --accent-soft: #e4f1ec;
  --accent-ink: #135c4a;
  --danger: #c2402f;
  --danger-soft: #faeae7;
  --warn: #a36a0a;
  --warn-soft: #faf0dc;
  --ok: #2e7d43;
  --ok-soft: #e8f3ea;

  --ch-airbnb: #d95a5f;
  --ch-airbnb-soft: #fbe9ea;
  --ch-booking: #2b62c4;
  --ch-booking-soft: #e8eefa;
  --ch-vrbo: #c08b1d;
  --ch-vrbo-soft: #f9f0dc;
  --ch-agoda: #a84a86;
  --ch-agoda-soft: #f7e9f2;
  --ch-direct: #1f7a63;
  --ch-direct-soft: #e4f1ec;
  --ch-block: #8a887f;
  --ch-block-soft: #efeeea;

  --sidebar: #182420;
  --sidebar-ink: #c8d4cf;
  --sidebar-active: #24352f;

  --radius: 10px;
  --radius-sm: 7px;
  --font: "Schibsted Grotesk", -apple-system, "Segoe UI", sans-serif;
  --shadow: 0 1px 2px rgba(28, 27, 24, 0.05), 0 4px 16px rgba(28, 27, 24, 0.05);
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #151714;
    --surface: #1e211d;
    --surface-2: #242723;
    --border: #30342e;
    --border-strong: #41463f;
    --ink: #e9e7e1;
    --ink-2: #a6a49c;
    --ink-3: #787670;
    --accent: #35a385;
    --accent-soft: #1e3a31;
    --accent-ink: #8ed4bc;
    --danger: #e0654f;
    --danger-soft: #3b241e;
    --warn: #d9a03c;
    --warn-soft: #37301d;
    --ok: #64bd7e;
    --ok-soft: #203526;

    --ch-airbnb: #e0666b;
    --ch-airbnb-soft: #3d2627;
    --ch-booking: #5b8ade;
    --ch-booking-soft: #212c40;
    --ch-vrbo: #d3a138;
    --ch-vrbo-soft: #3a301c;
    --ch-agoda: #c366a0;
    --ch-agoda-soft: #3a2333;
    --ch-direct: #35a385;
    --ch-direct-soft: #1e3a31;
    --ch-block: #8f8d85;
    --ch-block-soft: #2b2e2a;

    --sidebar: #101412;
    --sidebar-ink: #b9c6c0;
    --sidebar-active: #26332d;
    --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 4px 16px rgba(0, 0, 0, 0.35);
  }
}

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
  font-family: var(--font);
  font-size: 14px;
  color: var(--ink);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
}

/* ---------- app shell ---------- */
.shell { display: flex; min-height: 100vh; }
.main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
/* .main is a flex column, so everything directly inside it is a flex item and
   inherits min-width:auto - it will not shrink below its own content's
   min-content width, and instead pushes the page sideways. .main itself was
   already given min-width:0 for exactly this reason; its children were not,
   and cleaner-clean.html's .cl-wrap measured 372.6px inside a 365px .main.
   Fourth time this default has broken a layout here, so it is fixed for every
   child rather than for the page that happened to show it. */
.main > * { min-width: 0; }
.content { padding: 24px 28px 48px; flex: 1; min-width: 0; }

.sidebar {
  width: 224px; flex-shrink: 0; background: var(--sidebar); color: var(--sidebar-ink);
  display: flex; flex-direction: column; position: sticky; top: 0; height: 100vh;
}
.sidebar .brand {
  display: flex; align-items: center; gap: 10px; padding: 20px 18px 16px;
  color: #fff; font-weight: 600; font-size: 15px; letter-spacing: 0.2px;
}
.sidebar .brand .mark {
  width: 26px; height: 26px; border-radius: 8px; background: var(--accent);
  display: flex; align-items: center; justify-content: center; color: #fff; font-size: 13px; font-weight: 700;
}
/* min-height:0 lets the nav shrink inside the sidebar's 100vh box; without it
   a long nav pushes the identity chip (and its account menu) off the bottom
   of the window entirely, which is what happened to the admin portal on a
   625px-tall window. The nav scrolls, the chip stays reachable. */
.sidebar nav { padding: 6px 10px; display: flex; flex-direction: column; gap: 2px; flex: 1; min-height: 0; overflow-y: auto; }
.sidebar nav a {
  display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 8px;
  color: var(--sidebar-ink); text-decoration: none; font-size: 13.5px; font-weight: 500;
}
.sidebar nav a svg { width: 17px; height: 17px; stroke-width: 1.8; opacity: 0.85; }
.sidebar nav a:hover { background: rgba(255, 255, 255, 0.06); color: #fff; }
.sidebar nav a.active { background: var(--sidebar-active); color: #fff; }
.sidebar nav a .count {
  margin-left: auto; background: var(--accent); color: #fff; font-size: 11px;
  padding: 1px 7px; border-radius: 999px; font-weight: 600;
}
.sidebar .nav-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.8px; color: #6e7f78; padding: 14px 12px 4px; }
.sidebar .foot-menu-wrap { position: relative; }
.sidebar .foot { padding: 14px; border-top: 1px solid rgba(255, 255, 255, 0.08); display: flex; align-items: center; gap: 10px; cursor: pointer; }
.sidebar .foot:hover { background: rgba(255, 255, 255, 0.04); }
.sidebar .foot:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.sidebar .foot .avatar { width: 30px; height: 30px; border-radius: 50%; background: #3c5249; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 600; flex-shrink: 0; }
.sidebar .foot .who { line-height: 1.25; min-width: 0; }
.sidebar .foot .who b { display: block; color: #fff; font-size: 12.5px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sidebar .foot .who span { font-size: 11.5px; color: #6e7f78; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block; }
/* AC1: the footer identity menu's own header row - name/email/role, not a
   clickable menuitem (see app.js's populateMenu). Sits inside the same
   .menu popup as every other kebab/foot menu, so it inherits card colours
   (light surface even though the sidebar itself is always dark). */
.sidebar .foot-menu-wrap .menu { bottom: 100%; top: auto; left: 0; right: auto; margin-bottom: 6px; min-width: 220px; }
.menu-id { padding: 10px 12px 11px; border-bottom: 1px solid var(--border); margin-bottom: 5px; }
.menu-id b { display: block; font-size: 13px; color: var(--ink); }
.menu-id span { display: block; font-size: 11.5px; color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.menu-id .menu-id-role { display: inline-block; margin-top: 5px; font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.4px; color: var(--accent-ink); background: var(--accent-soft); padding: 2px 7px; border-radius: 999px; }

.topbar {
  display: flex; align-items: center; gap: 14px; padding: 18px 28px 0;
}
.topbar h1 { font-size: 19px; font-weight: 600; margin: 0; letter-spacing: -0.2px; }
.topbar .sub { color: var(--ink-2); font-size: 13px; margin-top: 2px; }
.topbar .spacer { flex: 1; }

/* ---------- primitives ---------- */
.card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); }
.card .card-head { display: flex; align-items: center; gap: 10px; padding: 14px 16px; border-bottom: 1px solid var(--border); }
.card .card-head h2 { font-size: 13.5px; font-weight: 600; margin: 0; }
.card .card-head .hint { color: var(--ink-3); font-size: 12px; }
.card .card-body { padding: 16px; }
.card .card-body.flush { padding: 0; }

.btn {
  display: inline-flex; align-items: center; gap: 7px; padding: 7px 13px; border-radius: 8px;
  border: 1px solid var(--border-strong); background: var(--surface); color: var(--ink);
  font-family: var(--font); font-size: 13px; font-weight: 500; cursor: pointer; text-decoration: none;
}
.btn:hover { background: var(--surface-2); }
.btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn.primary:hover { background: var(--accent-ink); }
.btn.danger { color: var(--danger); border-color: #e7c4bd; }
.btn.sm { padding: 4px 10px; font-size: 12.5px; border-radius: 7px; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }

.input, select.input, textarea.input {
  font-family: var(--font); font-size: 13.5px; color: var(--ink);
  border: 1px solid var(--border-strong); border-radius: 8px; padding: 8px 11px; background: var(--surface);
  outline: none; width: 100%;
}
.input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
label.field { display: block; font-size: 12.5px; font-weight: 500; color: var(--ink-2); margin-bottom: 5px; }

.badge {
  display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px; font-weight: 600;
  padding: 2px 9px; border-radius: 999px; white-space: nowrap;
}
.badge .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.badge.airbnb { background: var(--ch-airbnb-soft); color: var(--ch-airbnb); }
.badge.booking { background: var(--ch-booking-soft); color: var(--ch-booking); }
.badge.vrbo { background: var(--ch-vrbo-soft); color: var(--ch-vrbo); }
.badge.agoda { background: var(--ch-agoda-soft); color: var(--ch-agoda); }
.badge.direct { background: var(--ch-direct-soft); color: var(--ch-direct); }
.badge.block { background: var(--ch-block-soft); color: var(--ch-block); }
.badge.ok { background: var(--ok-soft); color: var(--ok); }
.badge.warn { background: var(--warn-soft); color: var(--warn); }
.badge.danger { background: var(--danger-soft); color: var(--danger); }
.badge.neutral { background: var(--ch-block-soft); color: var(--ink-2); }

.table { width: 100%; border-collapse: collapse; font-size: 13px; }
.table th {
  text-align: left; font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.5px;
  color: var(--ink-3); font-weight: 600; padding: 10px 16px; border-bottom: 1px solid var(--border);
}
.table td { padding: 11px 16px; border-bottom: 1px solid var(--border); vertical-align: middle; }
.table tr:last-child td { border-bottom: none; }
.table tr.rowlink { cursor: pointer; }
.table tr.rowlink:hover td { background: var(--surface-2); }
.table .num { text-align: right; font-variant-numeric: tabular-nums; }

.kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 14px; }
.kpi { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 14px 16px; }
.kpi .k-label { font-size: 12px; color: var(--ink-2); font-weight: 500; }
.kpi .k-value { font-size: 24px; font-weight: 600; letter-spacing: -0.4px; margin-top: 4px; font-variant-numeric: tabular-nums; }
.kpi .k-delta { font-size: 12px; margin-top: 3px; font-weight: 500; }
.kpi .k-delta.up { color: var(--ok); }
.kpi .k-delta.down { color: var(--danger); }
.kpi .k-delta.flat { color: var(--ink-3); }
/* Navigable widgets (app.js). Every box carries ‹ [period] › and, when it
   has more than one measure, a dot stepper. Permanently visible: hiding the
   controls until hover left people unsure a box could be moved at all. They
   are sized and coloured to read as furniture, not as calls to action. */
.kpi .k-head { display: flex; align-items: center; gap: 8px; min-height: 20px; }
.kpi .k-head .k-label { flex: 1; min-width: 0; }
.w-nav { display: inline-flex; align-items: center; gap: 1px; flex-shrink: 0; }
.card-nav .card-head .w-nav { margin-left: auto; }
.w-step, .w-face {
  border: 0; background: none; padding: 0 2px; color: var(--ink-3); cursor: pointer;
  font-size: 13px; line-height: 1; border-radius: 3px;
}
.w-step:hover:not(:disabled) { color: var(--ink); }
.w-step:disabled { opacity: 0.28; cursor: default; }
.w-step:focus-visible, .w-face:focus-visible, .w-period:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* The period itself is the control: a quiet chip, not a button that competes
   with the tile's own number. */
.w-period {
  border: 0; background: none; padding: 1px 3px; margin: 0; cursor: pointer;
  font: inherit; font-size: 11px; font-weight: 500; color: var(--ink-3);
  display: inline-flex; align-items: center; gap: 2px; white-space: nowrap;
  border-radius: 4px; max-width: 92px; overflow: hidden;
}
.w-period:hover { color: var(--ink); background: var(--surface-2); }
.w-period .menu-caret { width: 9px; height: 9px; opacity: 0.75; }
.card-nav .card-head .w-period { font-size: 12px; max-width: none; }
/* The measure stepper is the label itself, so it costs no width. It looks
   exactly like a label until hovered. */
.kpi .k-label.w-face {
  border: 0; background: none; padding: 0; margin: 0; font: inherit; text-align: left;
  color: var(--ink-2); cursor: pointer; border-bottom: 1px dashed transparent;
}
.kpi .k-label.w-face:hover { color: var(--ink); border-bottom-color: var(--ink-3); }
.kpi-nav:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* A tile is narrow, so its label gets the row above the value to itself and
   the controls sit on their own line beneath the sub text. */
.kpi-nav .k-head { flex-wrap: wrap; }

.avatar-sm { width: 28px; height: 28px; border-radius: 50%; background: var(--accent-soft); color: var(--accent-ink); display: inline-flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 600; flex-shrink: 0; }

.chip-row { display: flex; gap: 8px; flex-wrap: wrap; }
.chip {
  border: 1px solid var(--border-strong); background: var(--surface); border-radius: 999px;
  padding: 5px 13px; font-size: 12.5px; font-weight: 500; cursor: pointer; color: var(--ink-2); font-family: var(--font);
}
.chip.active { background: var(--ink); border-color: var(--ink); color: var(--bg); }

.empty { text-align: center; padding: 40px 20px; color: var(--ink-2); }
.empty h3 { font-size: 14px; margin: 0 0 4px; color: var(--ink); }
.empty p { font-size: 13px; margin: 0 0 14px; }

/* ---------- loading / error states ---------- */
.skeleton-wrap { padding: 16px; display: flex; flex-direction: column; gap: 10px; }
.skeleton {
  height: 14px; border-radius: 6px; background: linear-gradient(90deg, var(--border) 25%, var(--surface-2) 50%, var(--border) 75%);
  background-size: 200% 100%; animation: skeleton-pulse 1.4s ease-in-out infinite;
}
@keyframes skeleton-pulse { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }

.err-card {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  background: var(--danger-soft); color: var(--danger); border-radius: var(--radius);
  padding: 14px 16px; margin: 16px; font-size: 13px;
}

.toast-wrap { position: fixed; bottom: 22px; right: 22px; display: flex; flex-direction: column; gap: 8px; z-index: 60; }
.toast {
  background: #1c1b18; color: #fff; padding: 10px 16px; border-radius: 9px; font-size: 13px;
  box-shadow: var(--shadow); display: flex; align-items: center; gap: 9px; animation: toastin 0.18s ease-out;
}
.toast .t-ok { color: #7fd39a; }
@keyframes toastin { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
.stack { display: flex; flex-direction: column; gap: 16px; }
.row { display: flex; align-items: center; gap: 10px; }
.muted { color: var(--ink-2); }
.small { font-size: 12px; }
.mono { font-variant-numeric: tabular-nums; }

/* ---------- banners ---------- */
.banner { display: flex; align-items: center; gap: 12px; border-radius: var(--radius); padding: 12px 16px; flex-wrap: wrap; }
.banner.danger { background: var(--danger-soft); border: 1px solid var(--danger); }
.banner.warn { background: var(--warn-soft); border: 1px solid var(--warn); }
.banner.neutral { background: var(--accent-soft); border: 1px solid var(--accent); }
.banner .b-text { flex: 1; min-width: 220px; }
@media (max-width: 700px) { .banner .btn { width: 100%; justify-content: center; } }

/* ---------- list rows ---------- */
.listrow { display: flex; align-items: center; gap: 11px; padding: 11px 16px; border-bottom: 1px solid var(--border); }
.listrow:last-child { border-bottom: none; }
.listrow > .lr-main { flex: 1; min-width: 0; }
.lr-side { display: flex; gap: 8px; align-items: center; flex-shrink: 0; }

/* ---------- attention rows ---------- */
.attn-row { display: flex; gap: 11px; padding: 12px 16px; border-bottom: 1px solid var(--border); text-decoration: none; color: inherit; }
.attn-row:last-child { border-bottom: none; }
.attn-row .badge { flex-shrink: 0; }

/* ---------- multi-calendar ---------- */
.cal-wrap { overflow-x: auto; position: relative; }
.cal { display: grid; min-width: max-content; }
.cal .cal-corner, .cal .cal-prop {
  position: sticky; left: 0; z-index: 5; background: var(--surface); border-right: 1px solid var(--border);
}
.cal .cal-head { display: flex; }
.cal .cal-day {
  width: 46px; flex-shrink: 0; text-align: center; padding: 8px 0 6px; font-size: 11px; color: var(--ink-3);
  border-bottom: 1px solid var(--border); border-left: 1px solid var(--border); background: var(--surface);
  position: sticky; top: 0; z-index: 4;
}
.cal .cal-day b { display: block; font-size: 13px; color: var(--ink); font-weight: 600; }
.cal .cal-day.today { background: var(--accent-soft); }
.cal .cal-day.today b { color: var(--accent-ink); }
.cal .cal-day.wkend { background: var(--surface-2); }
.cal-row { display: flex; position: relative; height: 52px; border-bottom: 1px solid var(--border); }
.cal-prop { width: 190px; flex-shrink: 0; padding: 8px 12px; }
.cal-prop b { display: block; font-size: 12.5px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cal-prop span { font-size: 11px; color: var(--ink-3); }
.cal-cells { display: flex; position: relative; }
.cal-cell { width: 46px; flex-shrink: 0; border-left: 1px solid var(--border); position: relative; }
.cal-cell.wkend { background: var(--surface-2); }
.cal-cell.today { background: var(--accent-soft); opacity: 0.55; }
.cal-cell .rate { position: absolute; bottom: 2px; right: 4px; font-size: 9.5px; color: var(--ink-3); font-variant-numeric: tabular-nums; }
.cal-cell.selecting { background: var(--accent-soft); }
.res-bar {
  position: absolute; top: 7px; height: 26px; border-radius: 999px; color: #fff; font-size: 11.5px; font-weight: 600;
  display: flex; align-items: center; padding: 0 10px; white-space: nowrap; overflow: hidden; cursor: pointer; z-index: 2;
}
.res-bar.airbnb { background: var(--ch-airbnb); }
.res-bar.booking { background: var(--ch-booking); }
.res-bar.vrbo { background: var(--ch-vrbo); }
.res-bar.agoda { background: var(--ch-agoda); }
.res-bar.direct { background: var(--ch-direct); }
.res-bar.block { background: var(--ch-block-soft); color: var(--ink-2); border: 1px dashed var(--ch-block); font-weight: 500; }
.res-bar.conflict { outline: 2px solid var(--danger); outline-offset: 1px; top: 24px; height: 22px; z-index: 3; }
.cal-legend { display: flex; gap: 14px; flex-wrap: wrap; padding: 12px 16px; border-top: 1px solid var(--border); font-size: 12px; color: var(--ink-2); }
.cal-legend .lg { display: flex; align-items: center; gap: 6px; }
.cal-legend .sw { width: 10px; height: 10px; border-radius: 3px; }
.conflict-flag {
  position: absolute; top: 1px; z-index: 4; background: var(--danger); color: #fff; font-size: 9.5px; font-weight: 700;
  border-radius: 4px; padding: 0 5px; line-height: 14px; cursor: pointer;
}
.blockbar {
  position: sticky; left: 0; display: none; align-items: center; gap: 12px; padding: 10px 16px;
  background: #1c1b18; color: #fff; border-radius: 9px; margin: 12px; font-size: 13px; z-index: 6; width: fit-content;
}
.blockbar.show { display: flex; }

/* ---------- inbox ---------- */
/* The three panes each scroll internally, so the frame fills the window
   instead of stopping at a fixed height and leaving dead space below it on a
   large screen. The exact height is set from the frame's own top offset by
   fitInbox() in inbox.html — a calc() against 100vh has to guess how tall
   everything above it is, and guessed high it pushes the composer off the
   bottom of a short window. This value is only the pre-script fallback, and
   the floor stays under the shortest window we support for the same reason. */
.inbox { display: grid; grid-template-columns: 300px minmax(0, 1fr) 300px; gap: 0; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); overflow: hidden; min-height: 360px; height: calc(100dvh - 200px); }
/* Grid items default to min-height:auto, so each pane sizes to its full
   content (thousands of pixels of messages) and the frame's overflow:hidden
   clips it — the panes' own overflow-y never gets a chance to scroll. */
.inbox > * { min-height: 0; }
.threads { border-right: 1px solid var(--border); overflow-y: auto; }
.threads .t-filters { padding: 12px; border-bottom: 1px solid var(--border); }
.thread { display: flex; gap: 10px; padding: 12px 14px; border-bottom: 1px solid var(--border); cursor: pointer; }
.thread:hover { background: var(--surface-2); }
.thread.active { background: var(--accent-soft); }
.thread .t-main { min-width: 0; flex: 1; }
.thread .t-top { display: flex; align-items: baseline; gap: 8px; }
.thread .t-top b { font-size: 13px; font-weight: 600; }
.thread .t-top .when { margin-left: auto; font-size: 11px; color: var(--ink-3); flex-shrink: 0; }
.thread .t-prev { font-size: 12.5px; color: var(--ink-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 2px; }
.thread.unread .t-prev { color: var(--ink); font-weight: 500; }
.thread .t-meta { margin-top: 5px; display: flex; gap: 6px; align-items: center; }
.unread-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); flex-shrink: 0; margin-top: 6px; }

.convo { display: flex; flex-direction: column; min-width: 0; }
.convo-head { padding: 13px 18px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 10px; }
.convo-head .convo-who { flex: 1; min-width: 0; }
.convo-body { flex: 1; overflow-y: auto; padding: 18px; display: flex; flex-direction: column; gap: 12px; background: var(--surface-2); }
.msg { max-width: 72%; padding: 9px 13px; border-radius: 13px; font-size: 13.5px; line-height: 1.45; }
.msg.guest { background: var(--surface); border: 1px solid var(--border); align-self: flex-start; border-bottom-left-radius: 4px; }
.msg.host { background: var(--accent); color: #fff; align-self: flex-end; border-bottom-right-radius: 4px; }
.msg.auto { background: var(--accent-soft); color: var(--accent-ink); align-self: flex-end; border-bottom-right-radius: 4px; }
.msg .m-when { display: block; font-size: 10.5px; opacity: 0.7; margin-top: 4px; }
.msg .m-tag { display: inline-block; font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 3px; opacity: 0.75; }
.msg.removed { opacity: 0.65; }
.msg .m-removed-note { font-size: 10.5px; opacity: 0.75; font-style: italic; margin-top: 2px; }
.sched {
  align-self: flex-end; max-width: 72%; border: 1px dashed var(--border-strong); border-radius: 13px;
  padding: 9px 13px; font-size: 12.5px; color: var(--ink-2); background: var(--surface);
}
.sched b { color: var(--ink); }
.convo-reply { border-top: 1px solid var(--border); padding: 12px 14px; }
.convo-reply .tools { display: flex; gap: 8px; margin-top: 8px; align-items: center; }
.relay-note { background: var(--warn-soft); color: var(--warn); font-size: 12px; padding: 7px 12px; border-radius: 7px; margin-bottom: 8px; display: flex; gap: 7px; align-items: center; }

.ctx { border-left: 1px solid var(--border); overflow-y: auto; }
.ctx section { padding: 14px 16px; border-bottom: 1px solid var(--border); }
.ctx h3 { font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.6px; color: var(--ink-3); margin: 0 0 10px; font-weight: 600; }
.ctx .kv { display: flex; justify-content: space-between; font-size: 12.5px; padding: 3px 0; }
.ctx .kv span { color: var(--ink-2); }
.ctx .kv b { font-weight: 500; font-variant-numeric: tabular-nums; }

/* ---------- sync ledger ---------- */
.health-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 14px; }
.health { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 14px 16px; }
.health .h-top { display: flex; align-items: center; gap: 9px; margin-bottom: 8px; }
.health .h-top b { font-size: 13.5px; }
.health .h-route { font-size: 12px; color: var(--ink-2); margin-bottom: 10px; }
.health .h-stats { display: flex; gap: 14px; font-size: 12px; color: var(--ink-2); }
.health .h-stats b { display: block; font-size: 15px; color: var(--ink); font-variant-numeric: tabular-nums; }
.ch-dot { width: 9px; height: 9px; border-radius: 50%; }
.ch-dot.airbnb { background: var(--ch-airbnb); } .ch-dot.booking { background: var(--ch-booking); }
.ch-dot.vrbo { background: var(--ch-vrbo); } .ch-dot.agoda { background: var(--ch-agoda); }
.ch-dot.direct { background: var(--ch-direct); }

.diff { border: 1px solid var(--danger); border-radius: var(--radius); overflow: hidden; }
.diff .diff-head { background: var(--danger-soft); color: var(--danger); padding: 11px 16px; font-weight: 600; font-size: 13px; display: flex; align-items: center; gap: 8px; }
/* C11: a conflict where neither side is a real reservation (kind 'overlap',
   see sync.html renderConflict) is an availability clash, not a guest-impacting
   double booking - amber, never the red double-booking treatment. */
.diff.warn { border-color: var(--warn); }
.diff.warn .diff-head { background: var(--warn-soft); color: var(--warn); }
.diff .diff-body { display: grid; grid-template-columns: 1fr 1fr; }
.diff .diff-col { padding: 14px 16px; }
.diff .diff-col + .diff-col { border-left: 1px solid var(--border); }
.diff .diff-col h4 { margin: 0 0 8px; font-size: 12px; color: var(--ink-2); font-weight: 600; }
.diff .diff-actions { border-top: 1px solid var(--border); padding: 12px 16px; display: flex; flex-wrap: wrap; gap: 10px; }
.diff .diff-col { min-width: 0; }

/* ---------- tasks ---------- */
.board { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; align-items: start; }
.lane .lane-head { display: flex; align-items: center; gap: 8px; padding: 4px 4px 10px; font-size: 13px; font-weight: 600; }
.lane .lane-head .n { color: var(--ink-3); font-weight: 500; }
.task { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 12px 14px; margin-bottom: 10px; }
.task .tk-top { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
.task .tk-top b { font-size: 13px; }
.task .tk-meta { font-size: 12px; color: var(--ink-2); display: flex; gap: 10px; align-items: center; margin-top: 8px; }
.task.done { opacity: 0.6; }
.tk-check { display: flex; gap: 7px; align-items: center; font-size: 12.5px; color: var(--ink-2); padding: 2px 0; }

/* ---------- misc pages ---------- */
.photo-ph { border-radius: var(--radius-sm); aspect-ratio: 4/3; background: linear-gradient(135deg, #dfe8e3, #c6d6cd); }
.photo-ph.alt-1 { background: linear-gradient(135deg, #e8e2d5, #d5c9ae); }
.photo-ph.alt-2 { background: linear-gradient(135deg, #dde3ea, #b9c6d4); }
.conn-step { display: flex; gap: 12px; padding: 13px 0; border-bottom: 1px solid var(--border); }
.conn-step:last-child { border-bottom: none; }
.conn-step .s-n { width: 24px; height: 24px; border-radius: 50%; background: var(--accent-soft); color: var(--accent-ink); font-size: 12px; font-weight: 600; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.conn-step .s-n.done { background: var(--accent); color: #fff; }

.drawer-veil { position: fixed; inset: 0; background: rgba(24, 23, 20, 0.35); z-index: 40; display: none; }
.drawer-veil.open { display: block; }
.drawer {
  position: fixed; top: 0; right: 0; bottom: 0; width: 460px; max-width: 92vw; background: var(--surface);
  z-index: 50; transform: translateX(100%); transition: transform 0.22s ease; overflow-y: auto;
  border-left: 1px solid var(--border); box-shadow: -8px 0 32px rgba(28, 27, 24, 0.12);
}
.drawer.open { transform: none; }
.drawer .d-head { padding: 16px 20px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 10px; position: sticky; top: 0; background: var(--surface); }
.drawer .d-body { padding: 18px 20px; }

@media (max-width: 1100px) {
  .inbox { grid-template-columns: 260px minmax(0, 1fr); }
  .ctx { display: none; }
  .grid-2, .grid-3, .board { grid-template-columns: 1fr; }
}
/* Below .veil / .drawer-veil (both 40): an open modal dims the nav and takes
   its taps, instead of leaving a lit bar that navigates away from the form
   the reader is filling in. */
#mobilenav {
  display: none;
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 39;
  padding-bottom: env(safe-area-inset-bottom);
  background: var(--sidebar); border-top: 1px solid rgba(255, 255, 255, 0.08);
}
#mobilenav a {
  flex: 1; display: flex; flex-direction: column; align-items: center; gap: 3px;
  padding: 8px 4px 10px; color: var(--sidebar-ink); text-decoration: none; font-size: 10px; font-weight: 500;
}
#mobilenav a svg { width: 20px; height: 20px; stroke-width: 1.8; }
#mobilenav a.active { color: #fff; }
#mobilenav a.active svg { color: var(--accent); }

@media (max-width: 820px) {
  .sidebar { display: none; }
  .content { padding: 16px; grid-template-columns: 1fr !important; }
  .topbar { flex-wrap: wrap; row-gap: 10px; padding: 16px 16px 0; }
  .inbox { grid-template-columns: 1fr; height: auto; }
  .threads { max-height: 300px; }
  #mobilenav { display: flex; }
  /* Hung off the bar's presence rather than the width: signin, set-password
     and the onboarding wizard never build a bottom bar, and were reserving
     56px of empty page for something that was not there. */
  body:has(#mobilenav) { padding-bottom: calc(56px + env(safe-area-inset-bottom)); }
  /* A page whose main surface is sized to the viewport (the inbox) subtracts
     the bottom bar itself, so the page must not also reserve room for it —
     that reserved strip is the only reason the page scrolls at all. It still
     scrolls if something above the frame overflows; hiding that would put the
     composer permanently out of reach. */
  body.app-frame:has(#mobilenav) { padding-bottom: 0; }
  /* fitInbox sizes the frame to the room left; the desktop floor would
     override that and push the composer off the bottom of a phone. */
  .inbox { min-height: 0; }
  /* A toast is 38px tall and was landing inside the bottom bar's band, on top
     of it, killing the last two or three tabs for its whole lifetime - and
     every save fires one. */
  .toast-wrap { bottom: calc(56px + env(safe-area-inset-bottom) + 12px); left: 12px; right: 12px; pointer-events: none; }
  .toast-wrap > * { pointer-events: auto; }
}

@media (max-width: 600px) {
  .banner { flex-wrap: wrap; }
  .banner .b-actions { width: 100%; display: flex; justify-content: flex-end; }
}

@media (max-width: 480px) {
  .listrow { flex-wrap: wrap; }
  .listrow .lr-side { flex-basis: 100%; }
  /* Only rows that actually lead with an avatar need the indent that lines
     the wrapped side content up under it — a row with no avatar has nothing
     to line up with. */
  .listrow.has-avatar .lr-side { margin-left: 39px; }
  .attn-row { flex-wrap: wrap; }
  #perm-save-row { flex-wrap: wrap; }
  #perm-save-row .input { width: 100% !important; }
}

@media (prefers-color-scheme: dark) {
  .photo-ph { background: linear-gradient(135deg, #2c3a34, #22302a); }
  .photo-ph.alt-1 { background: linear-gradient(135deg, #3a352b, #2c2820); }
  .photo-ph.alt-2 { background: linear-gradient(135deg, #232a30, #1c232a); }
  .toast { background: #2b2e2a; }
}

/* ---------- calendar: date navigation + editable pricing (pricing engine / multi-calendar) ---------- */
.cal-nav {
  display: flex; align-items: center; gap: 10px; padding: 12px 16px; border-bottom: 1px solid var(--border); flex-wrap: wrap;
}
.cal-nav-group { display: flex; gap: 6px; }
.cal-nav-range { font-size: 12.5px; color: var(--ink-2); font-variant-numeric: tabular-nums; white-space: nowrap; }

.cal-cell .rate {
  position: absolute; bottom: 2px; right: 4px; font-size: 9.5px; color: var(--ink-3); font-variant-numeric: tabular-nums;
  cursor: pointer; padding: 1px 2px; border-radius: 3px; z-index: 1;
}
.cal-cell .rate:hover { background: var(--surface); color: var(--ink); box-shadow: 0 0 0 1px var(--border-strong); }
.cal-cell .rate.override { color: var(--accent-ink); border-bottom: 1.5px solid var(--accent); font-weight: 600; }
.cal-cell .rate sup { font-size: 8px; margin-left: 1px; opacity: 0.85; }
.cal-cell.closed {
  background-image: repeating-linear-gradient(135deg, var(--danger-soft) 0 6px, transparent 6px 12px);
}
.cal-cell .rate-edit {
  position: absolute; bottom: 1px; right: 2px; width: 40px; font-size: 10px; padding: 1px 3px; border-radius: 4px;
  border: 1px solid var(--accent); outline: none; z-index: 3; font-family: var(--font); font-variant-numeric: tabular-nums;
  background: var(--surface); color: var(--ink);
}
.blockbar-sep { width: 1px; align-self: stretch; background: rgba(255, 255, 255, 0.18); margin: 0 2px; }
.blockbar .input { color: #fff; }

.cal-legend .lg-rate {
  font-size: 10px; color: var(--ink-3); font-variant-numeric: tabular-nums; padding: 1px 3px;
}
.cal-legend .lg-rate.override { color: var(--accent-ink); border-bottom: 1.5px solid var(--accent); font-weight: 600; }
.cal-legend .lg-rate sup { font-size: 8px; }
.cal-legend .lg-closed-swatch {
  width: 14px; height: 10px; border-radius: 3px; flex-shrink: 0;
  background-image: repeating-linear-gradient(135deg, var(--danger-soft) 0 4px, transparent 4px 8px);
  border: 1px solid var(--danger-soft);
}

@media (prefers-color-scheme: dark) {
  .cal-cell.closed { background-image: repeating-linear-gradient(135deg, var(--danger-soft) 0 6px, transparent 6px 12px); }
}

@media (max-width: 820px) {
  .cal-nav { padding: 10px 12px; }
  .cal-nav-range { order: 10; width: 100%; }
}

/* Pricing authority (keystay / follows a channel / independent) */
.cal-cell .rate.readonly { cursor: default; opacity: 0.7; }
.cal-cell .rate.readonly:hover { background: none; box-shadow: none; color: var(--ink-3); }
.prop-authority-tag {
  display: inline-block; font-size: 9.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.4px;
  color: var(--ink-3); background: var(--surface-2); border: 1px solid var(--border); border-radius: 999px;
  padding: 1px 7px; margin-left: 6px; vertical-align: middle; white-space: nowrap;
}
.blockbar .prop-authority-tag { color: #cfcdc7; background: rgba(255, 255, 255, 0.08); border-color: rgba(255, 255, 255, 0.18); }

/* ============================================================
   Evidence (task photos) — append-only section, tasks.html /
   reservations.html. Tokens only, so the existing dark-mode
   :root override above covers this automatically.
   ============================================================ */

.evi-strip { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }

.evi-thumb {
  position: relative; width: 52px; height: 52px; border-radius: var(--radius-sm);
  overflow: hidden; border: 1px solid var(--border); background: var(--surface-2); flex-shrink: 0;
  padding: 0; cursor: pointer;
}
.evi-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.evi-thumb:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.evi-more {
  display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 600;
  color: var(--ink-2); background: var(--surface-2);
}

.evi-dropzone {
  display: flex; align-items: center; gap: 8px; margin-top: 8px; padding: 10px 12px;
  border: 1.5px dashed var(--border-strong); border-radius: var(--radius-sm);
  color: var(--ink-2); font-size: 12.5px; cursor: pointer; background: transparent;
  transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease;
}
.evi-dropzone svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width: 1.8; flex-shrink: 0; }
.evi-dropzone:hover { border-color: var(--accent); color: var(--accent-ink); }
.evi-dropzone:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.evi-dropzone.drag { border-color: var(--accent); background: var(--accent-soft); color: var(--accent-ink); }
.evi-dropzone.busy { pointer-events: none; opacity: 0.7; }
.evi-dropzone input[type="file"] { display: none; }

  position: absolute; top: 18px; right: 22px; width: 36px; height: 36px; border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.3); background: rgba(0, 0, 0, 0.35); color: #fff;
  font-size: 18px; line-height: 1; cursor: pointer;
}

.evi-day { border: 1px solid var(--border); border-radius: var(--radius-sm); overflow: hidden; }
.evi-day + .evi-day { margin-top: 10px; }
.evi-day-head { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: var(--surface-2); font-size: 12.5px; font-weight: 600; }
.evi-day-head .n { color: var(--ink-3); font-weight: 500; }
.evi-day-body { display: grid; grid-template-columns: repeat(auto-fill, minmax(72px, 1fr)); gap: 6px; padding: 10px 12px; }
.evi-day-body .evi-thumb { width: 100%; height: 72px; }
.evi-photo-task { font-size: 10.5px; color: var(--ink-3); margin-top: 2px; }

@media (max-width: 480px) {
  .evi-day-body { grid-template-columns: repeat(auto-fill, minmax(60px, 1fr)); }
}

/* ---------- onboarding wizard (append-only) ---------- */
/* width:100% for the same reason as .cl-wrap: auto side margins stop a flex
   item stretching, so without it this sizes to content rather than to its
   container. */
.wizard-shell { width: 100%; max-width: 760px; margin: 0 auto; padding: 40px 20px 80px; box-sizing: border-box; }
.wizard-brand { display: flex; align-items: center; gap: 10px; font-weight: 600; font-size: 15px; margin-bottom: 28px; }
.wizard-brand .mark { width: 26px; height: 26px; border-radius: 8px; background: var(--accent); display: flex; align-items: center; justify-content: center; color: #fff; font-size: 13px; font-weight: 700; flex-shrink: 0; }

.rail { display: flex; gap: 6px; margin-bottom: 8px; }
.rail .rail-step { flex: 1; height: 4px; border-radius: 999px; background: var(--border); }
.rail .rail-step.done, .rail .rail-step.active { background: var(--accent); }
.rail-labels { display: flex; justify-content: space-between; font-size: 11px; color: var(--ink-3); margin: 0 0 24px; }
.rail-labels span.current { color: var(--accent-ink); font-weight: 600; }

.wizard-head { margin-bottom: 20px; }
.wizard-head h1 { font-size: 22px; margin: 0 0 6px; letter-spacing: -0.3px; }
.wizard-head p { color: var(--ink-2); font-size: 13.5px; margin: 0; }

.channel-card { display: flex; align-items: center; gap: 14px; padding: 16px; }
.channel-card + .channel-card { margin-top: 12px; }
.channel-card .ch-mark {
  width: 40px; height: 40px; border-radius: 10px; flex-shrink: 0; display: flex;
  align-items: center; justify-content: center; font-weight: 700; font-size: 13px; color: #fff;
}
.channel-card .ch-mark.airbnb { background: var(--ch-airbnb); }
.channel-card .ch-mark.booking { background: var(--ch-booking); }
.channel-card .ch-mark.vrbo { background: var(--ch-vrbo); }
.channel-card .ch-body { flex: 1; min-width: 0; }
.channel-card .ch-body b { font-size: 14px; }
.channel-card .ch-body .small { margin-top: 3px; display: block; }

.listing-card { display: flex; gap: 14px; padding: 14px 16px; align-items: center; }
.listing-card + .listing-card { border-top: 1px solid var(--border); }
.listing-card input[type="checkbox"] { width: 18px; height: 18px; flex-shrink: 0; }
.listing-card .lc-photo { width: 76px; flex-shrink: 0; }
.listing-card .lc-body { flex: 1; min-width: 0; }
.listing-card .lc-body b { font-size: 13.5px; display: block; }

.receipt-stream { display: flex; flex-direction: column; }
.receipt-row { display: flex; align-items: center; gap: 10px; padding: 9px 16px; border-bottom: 1px solid var(--border); font-size: 12.5px; animation: toastin 0.18s ease-out; }
.receipt-row:last-child { border-bottom: none; }
.receipt-row .r-op { font-weight: 600; flex-shrink: 0; }
.receipt-row .r-detail { color: var(--ink-3); font-size: 11.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.invite-row { display: grid; grid-template-columns: 1.2fr 1.4fr 1fr auto; gap: 8px; margin-bottom: 8px; align-items: center; }
.wizard-actions { display: flex; gap: 10px; margin-top: 20px; justify-content: flex-end; }

@media (max-width: 600px) {
  .wizard-shell { padding: 24px 14px 60px; }
  .invite-row { grid-template-columns: 1fr; }
  .channel-card { flex-wrap: wrap; }
}

/* ---------- assistant typing indicator (append-only) ---------- */
.msg.typing { display: flex; gap: 4px; align-items: center; padding: 12px 14px; }
.msg.typing .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; opacity: 0.3; animation: typing-blink 1.1s infinite ease-in-out; }
.msg.typing .dot:nth-child(2) { animation-delay: 0.18s; }
.msg.typing .dot:nth-child(3) { animation-delay: 0.36s; }
@keyframes typing-blink { 0%, 80%, 100% { opacity: 0.25; } 40% { opacity: 1; } }

/* ---------- saved replies panel, inbox.html (append-only) ---------- */
.sr-item { padding: 10px 14px; border-bottom: 1px solid var(--border); cursor: pointer; }
.sr-item:hover { background: var(--surface-2); }
.sr-item:last-child { border-bottom: none; }
.sr-item .sr-title { font-weight: 600; font-size: 12.5px; display: flex; align-items: center; gap: 8px; }
.sr-item .sr-body { font-size: 12px; color: var(--ink-2); margin-top: 3px; white-space: pre-wrap; }
.sr-item .sr-del { margin-left: auto; color: var(--ink-3); background: none; border: none; cursor: pointer; font-size: 15px; line-height: 1; padding: 0 2px; }
.sr-item .sr-del:hover { color: var(--danger); }

/* ---------- mode chip, sidebar brand (append-only) ---------- */
.mode-chip { margin-left: auto; font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.4px; padding: 2px 7px; border-radius: 999px; }
.mode-chip.demo { background: var(--warn-soft); color: var(--warn); }
.mode-chip.live { background: var(--ok-soft); color: var(--ok); }

/* ---------- message attachments, inbox.html (append-only) ---------- */
.msg-atts { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.msg-att-thumb {
  display: block; width: 72px; height: 72px; padding: 0; cursor: pointer;
  border-radius: var(--radius-sm); overflow: hidden; border: 1px solid rgba(255, 255, 255, 0.25);
  background: none;
}
.msg-att-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* A message that is ONLY a photo shows it at a readable size and keeps its
   aspect ratio — a 72px square crop of a broken chair shows neither the chair
   nor the break. Matches how the channel's own app presents it. */
.msg-atts.one { margin-top: 0; }
.msg-atts.one .msg-att-thumb { width: auto; height: auto; max-width: min(260px, 68vw); border-radius: 10px; }
.msg-atts.one .msg-att-thumb img { width: 100%; height: auto; max-height: 340px; object-fit: contain; }
.msg-att-thumb:hover { border-color: rgba(255, 255, 255, 0.6); }
.msg-att-chip { display: inline-flex; align-items: center; font-size: 11.5px; font-weight: 600; padding: 4px 10px; border-radius: 999px; background: rgba(255, 255, 255, 0.15); color: inherit; text-decoration: none; }
.msg.guest .msg-att-chip { background: var(--surface-2); }

/* ---------- rendered AI markdown, renderMarkdown() (append-only) ---------- */
.md-h { font-weight: 600; margin-top: 8px; }
.md-h:first-child { margin-top: 0; }
#briefing-text div + div, #briefing-text ul, #briefing-text ol,
.msg div + div, .msg ul, .msg ol { margin-top: 8px; }
#briefing-text li, .msg li { margin-left: 18px; }
#briefing-text code, .msg code { background: var(--surface-2); border-radius: 4px; padding: 1px 5px; font-size: 0.92em; }
.pending-att-remove { cursor: pointer; margin-left: 4px; }

/* ============================================================
   Shared interaction primitives (append-only, L0). Ported verbatim
   from frontend/mocks/*.html's final versions: kebab+menu and modal+veil
   (dashboard-v2.html / cleaner-dashboard.html — identical in both), pager /
   scrollcard / trunc / totals-strip / alltime / mobilenav
   (reservations-list-v2.html), money-waterfall flow (dashboard-v2.html),
   accent badge (tasks-admin.html), day-cell (cleaner-dashboard.html).
   Tokens only — the mocks' rgba() overlay/shadow values aren't hex and
   already match the existing .drawer-veil / .viewer convention of
   staying constant across themes.
   ============================================================ */

.kebab { border: none; background: none; color: var(--ink-3); cursor: pointer; padding: 5px; border-radius: 6px; display: inline-flex; vertical-align: middle; }
.kebab:hover { background: var(--surface-2); color: var(--ink); }
.menu { position: absolute; right: 0; top: 100%; background: var(--surface); border: 1px solid var(--border); border-radius: 10px; box-shadow: 0 6px 24px rgba(0, 0, 0, 0.25); z-index: 60; min-width: 200px; padding: 5px; display: none; }
.menu.open { display: block; }
.menu button, .menu a { display: flex; align-items: center; gap: 9px; width: 100%; text-align: left; border: none; background: none; font-family: var(--font); font-size: 13px; color: var(--ink); padding: 8px 10px; border-radius: 7px; cursor: pointer; text-decoration: none; }
.menu button:hover, .menu a:hover { background: var(--surface-2); }
.menu .sep { height: 1px; background: var(--border); margin: 5px 4px; }

.viewall { margin-left: auto; font-size: 12.5px; color: var(--accent-ink); text-decoration: none; font-weight: 600; white-space: nowrap; }
.viewall:hover { text-decoration: underline; }

.veil { position: fixed; inset: 0; background: rgba(10, 12, 10, 0.45); display: none; z-index: 40; }
.veil.open { display: block; }
/* Capped and scrollable at the primitive. Uncapped, a modal taller than the
   viewport was centred with its top at a negative offset, and being fixed it
   could not be scrolled to: the task detail's title and first checklist items
   were simply unreachable on a phone. reservation.html had hand-patched its
   own dialog with an inline max-height, which is the tell that the primitive
   was wrong rather than the caller. */
.modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: min(480px, 92vw); max-height: 88dvh; background: var(--surface); border: 1px solid var(--border); border-radius: 12px; z-index: 50; display: none; box-shadow: 0 8px 40px rgba(0, 0, 0, 0.35); }
.modal.open { display: flex; flex-direction: column; }
.modal .m-head, .modal .m-foot { flex-shrink: 0; }
.modal .m-body { overflow-y: auto; min-height: 0; }
.modal .m-head { display: flex; align-items: center; gap: 10px; padding: 14px 16px; border-bottom: 1px solid var(--border); }
.modal .m-head h3 { margin: 0; font-size: 14px; font-weight: 600; }
.modal .m-body { padding: 4px 0; }
.modal .m-foot { display: flex; gap: 9px; padding: 12px 16px; border-top: 1px solid var(--border); flex-wrap: wrap; }
.mrow { display: flex; align-items: center; gap: 10px; padding: 10px 16px; border-bottom: 1px solid var(--border); font-size: 13.5px; }
.mrow:last-child { border-bottom: none; }

.pager { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; row-gap: 8px; padding: 11px 16px; border-top: 1px solid var(--border); font-size: 12.5px; color: var(--ink-2); }
.pager .pspacer { flex: 1; }
.pager select { font-family: var(--font); font-size: 12.5px; border: 1px solid var(--border-strong); border-radius: 7px; padding: 3px 8px; background: var(--surface); color: var(--ink); }
.pager button { border: 1px solid var(--border-strong); background: var(--surface); border-radius: 7px; padding: 3px 10px; font-family: var(--font); font-size: 13px; color: var(--ink); cursor: pointer; }
.pager button:disabled { opacity: 0.4; cursor: default; }

/* Lists grow with the window instead of sitting in a fixed 430px box on a
   tall screen. The floor keeps short windows usable; the viewport term is
   what lets a big monitor actually show more rows. */
.scrollcard { max-height: max(430px, calc(100vh - 360px)); overflow-y: auto; scrollbar-gutter: stable; }
.scrollcard thead th, .scrollcard tr:first-child th { position: sticky; top: 0; background: var(--surface); z-index: 2; }

.trunc { max-width: 190px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: inline-block; vertical-align: middle; }

.totals-strip { display: flex; gap: 0; flex-wrap: wrap; border-top: 1px solid var(--border); }
.totals-strip .t { padding: 11px 16px; border-right: 1px solid var(--border); font-size: 12px; color: var(--ink-2); }
.totals-strip .t:last-child { border-right: 0; }
.totals-strip .t b { display: block; font-size: 15px; color: var(--ink); font-variant-numeric: tabular-nums; margin-top: 1px; }

.alltime { display: flex; gap: 14px; flex-wrap: wrap; align-items: baseline; padding: 13px 16px; font-size: 12.5px; color: var(--ink-2); }
.alltime b { color: var(--ink); font-variant-numeric: tabular-nums; }

.table td.open-cell { white-space: nowrap; text-align: right; }
.table .open-cell a { color: var(--accent-ink); text-decoration: none; font-weight: 600; font-size: 12.5px; white-space: nowrap; }
.table .open-cell a:hover { text-decoration: underline; }

.flow { display: flex; align-items: stretch; overflow-x: auto; }
.flow .seg { padding: 14px 18px; border-right: 1px solid var(--border); min-width: 136px; flex: 1; }
.flow .seg:last-child { border-right: 0; background: var(--accent-soft); border-radius: 0 0 var(--radius) 0; }
.flow .f-label { font-size: 11.5px; color: var(--ink-2); font-weight: 600; }
.flow .f-amt { font-size: 19px; font-weight: 600; margin-top: 3px; font-variant-numeric: tabular-nums; letter-spacing: -0.3px; }
.flow .f-sub { font-size: 11.5px; color: var(--ink-3); margin-top: 2px; }
.flow .seg.deduct .f-amt { color: var(--danger); }
.flow .seg:last-child .f-amt { color: var(--accent-ink); }

.badge.accent { background: var(--accent-soft); color: var(--accent-ink); }

/* Scoped horizontal-scroll wrapper for wide tables inside a flush card body
   — same convention as .cal-wrap, deliberately NOT a blanket overflow-x on
   .card-body.flush itself (that would also compute overflow-y to auto,
   clipping any .menu popup that lives inside one of those card bodies). */
.table-scroll { overflow-x: auto; }

/* ---------- revenue-by-month bars (dashboard v2) ---------- */
.months { display: flex; gap: 6px; align-items: flex-end; height: 120px; padding: 16px 16px 6px; }
.months .m { flex: 1; display: flex; flex-direction: column; justify-content: flex-end; gap: 2px; min-width: 24px; height: 100%; }
.months .bar { border-radius: 4px 4px 0 0; background: var(--border-strong); min-height: 2px; }
.months .bar.net { background: var(--accent); }
.months .m.future .bar.net { background: var(--accent-soft); border: 1px solid var(--accent); border-bottom: 0; }
.mlabels { display: flex; gap: 6px; padding: 0 16px 12px; }
.mlabels span { flex: 1; text-align: center; font-size: 10.5px; color: var(--ink-3); min-width: 24px; }
.legend { display: flex; gap: 14px; padding: 0 16px 12px; font-size: 11.5px; color: var(--ink-2); flex-wrap: wrap; }
.legend i { display: inline-block; width: 10px; height: 10px; border-radius: 3px; margin-right: 5px; vertical-align: -1px; }

/* ---------- next-7-days schedule (dashboard v2) ---------- */
.sched-day { display: flex; gap: 12px; padding: 10px 16px; border-bottom: 1px solid var(--border); align-items: baseline; }
.sched-day:last-child { border-bottom: none; }
.sched-day .d { width: 52px; flex-shrink: 0; font-weight: 600; font-size: 12.5px; }
.sched-day .d span { display: block; font-weight: 400; color: var(--ink-3); font-size: 11px; }
.sched-day .ev { flex: 1; display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.ev-line { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 13px; }

/* ---------- channel health strip (dashboard v2) ---------- */
.health-strip { display: flex; flex-wrap: wrap; gap: 8px; padding: 14px 16px; }
/* A note turns the chip into a small block: a pill cannot carry a sentence,
   and the sentence is the part that says why a channel is behind. */
.hchip { display: inline-flex; align-items: center; gap: 7px; border: 1px solid var(--border); border-radius: 999px; padding: 5px 12px; font-size: 12px; background: var(--surface); }
.hchip:has(.h-note) { display: flex; align-items: flex-start; border-radius: 12px; padding: 9px 12px; flex-wrap: wrap; }
.hchip:has(.h-note) .st { margin-top: 5px; }
.hchip .h-main { min-width: 0; }
.hchip .h-note { flex-basis: 100%; color: var(--ink-2); font-size: 11.5px; line-height: 1.45; padding-left: 14px; }
@media (max-width: 820px) { .health-strip { flex-direction: column; align-items: stretch; } }
.hchip .st { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
.hchip .st.ok { background: var(--ok); }
.hchip .st.warn { background: var(--warn); }
.hchip .st.pending { background: var(--ink-3); }

.day { border: 1px solid var(--border); border-radius: 9px; padding: 8px 4px; font-size: 11.5px; text-align: center; color: var(--ink-2); }
.day b { display: block; font-size: 12.5px; color: var(--ink); }
.day.has { border-color: var(--accent); background: var(--accent-soft); }
.day.has .n { color: var(--accent-ink); font-weight: 600; }
.day .n, .day span { display: block; margin-top: 2px; }
.day.off { background: var(--ch-block-soft); color: var(--ink-3); }

.mobilenav { display: none; }

@media (max-width: 820px) {
  /* Wraps rather than scrolls: half a dozen pills fit on two lines, and a
     sideways scroller hides the last destination behind an edge nobody
     thinks to drag. */
  .mobilenav { display: flex; flex-wrap: wrap; gap: 8px; padding: 10px 14px; border-bottom: 1px solid var(--border); background: var(--surface); }
  .mobilenav a { padding: 6px 13px; border-radius: 999px; border: 1px solid var(--border-strong); color: var(--ink-2); text-decoration: none; font-size: 12.5px; font-weight: 500; white-space: nowrap; }
  .mobilenav a.active { background: var(--ink); border-color: var(--ink); color: var(--bg); }
}

.chart-scroll { overflow-x: auto; }
.chart-scroll .months, .chart-scroll .mlabels { min-width: 360px; }

/* Ledger tables: the period currently in view, a group heading inside the
   body, and rows that are records rather than periods (one-off exports). */
.table tr.row-selected > td { background: var(--accent-soft); }
.table tr.subhead > td {
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--ink-2);
  background: var(--surface-2); padding-top: 10px; padding-bottom: 6px;
}
.table tr.muted-row > td { color: var(--ink-2); }
/* Per-row contextual menu and dismiss. Quiet until the row is hovered, so a
   long table isn't a wall of controls. */
/* Row controls are always visible. Revealing them on hover left a gap that
   was occupied by nothing and told the reader nothing (owner, 2026-08-22) —
   the space is reserved either way, so it may as well hold the control.
   Muted until hovered, so a long table still reads as data. */
.row-menu { display: inline-flex; vertical-align: middle; margin-left: 4px; }
.row-menu .kebab { color: var(--ink-3); }
.row-menu .kebab:hover { color: var(--ink); }
.row-x { padding-left: 8px; padding-right: 8px; color: var(--ink-3); }
.row-x:hover { color: var(--danger); border-color: var(--danger); }
.linkish {
  background: none; border: 0; padding: 0; font: inherit; color: inherit; cursor: pointer;
  border-bottom: 1px dashed transparent;
}
.linkish:hover { color: var(--ink); border-bottom-color: var(--ink-2); }
.linkish:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ---------- period picker (assets/period.js) ---------- */
/* One control wherever a money surface is scoped to a period: ‹ ›, the
   current period as a menu, and direct entry for the kind in play. */
.periodbar { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.periodbar-step { min-width: 30px; padding-left: 8px; padding-right: 8px; font-size: 15px; line-height: 1; }
.periodbar-step:disabled { opacity: 0.4; cursor: default; }
.periodbar-current { font-weight: 600; display: inline-flex; align-items: center; gap: 5px; }
.periodbar-entry { width: auto; padding: 5px 9px; font-size: 12.5px; }
.periodbar-sep { font-size: 12px; color: var(--ink-2); }
.menu-caret { opacity: 0.6; flex-shrink: 0; }

/* ---------- pannable chart window ---------- */
/* Arrows that move a chart's month window, and the motion that makes the
   move readable: content enters from the side it was panned towards, so a
   step left feels like the earlier months sliding in from the left. */
.panbar { display: inline-flex; align-items: center; gap: 4px; margin-left: auto; }
.panbtn {
  width: 26px; height: 26px; padding: 0; display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--border-strong); border-radius: var(--radius-sm); background: var(--surface);
  color: var(--ink-2); cursor: pointer; font-size: 14px; line-height: 1;
}
.panbtn:hover:not(:disabled) { color: var(--ink); border-color: var(--ink-2); }
.panbtn:disabled { opacity: 0.35; cursor: default; }
.panbtn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
@keyframes pan-in-prev { from { opacity: 0; transform: translateX(-14px); } to { opacity: 1; transform: none; } }
@keyframes pan-in-next { from { opacity: 0; transform: translateX(14px); } to { opacity: 1; transform: none; } }
/* Measures move on the other axis from time, so the motion says which axis
   you just moved on. */
@keyframes pan-in-up { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
.pan-in-prev { animation: pan-in-prev 220ms cubic-bezier(0.22, 1, 0.36, 1); }
.pan-in-next { animation: pan-in-next 220ms cubic-bezier(0.22, 1, 0.36, 1); }
.pan-in-up { animation: pan-in-up 200ms cubic-bezier(0.22, 1, 0.36, 1); }
/* Deliberately no transition on bar height. Both charts rebuild their bars
   with innerHTML on every render, so the bars are new elements each time and
   a height transition could never fire anyway — and animating height is a
   layout property, which janks. The motion on a window change comes from the
   transform/opacity keyframes above, which composite. */
@media (prefers-reduced-motion: reduce) {
  .pan-in-prev, .pan-in-next, .pan-in-up { animation: none; }
}
/* The chart panner stays put like every other control. */
.panbar .panbtn { color: var(--ink-3); }
.panbar .panbtn:hover:not(:disabled) { color: var(--ink); }

/* ============================================================
   Cleaner-facing pages (L6f) — append-only. Ported from the approved
   frontend/mocks/cleaner-*.html set: fortnight/week grid (.week, alongside
   the already-shared .day above), clean-detail hero/checklist/supplies
   (cleaner-v2.html), account settings rows (cleaner-account.html).
   ============================================================ */

.week { display: grid; grid-template-columns: repeat(7, 1fr); gap: 6px; padding: 14px 16px; }
button.day { font-family: var(--font); cursor: pointer; }

.hero { display: flex; justify-content: space-between; gap: 14px; flex-wrap: wrap; }
.hero h2 { margin: 0 0 4px; font-size: 16px; }
.times { display: flex; gap: 10px; }
.timebox { text-align: center; border: 1px solid var(--border); border-radius: 10px; padding: 9px 16px; background: var(--surface-2); }
.timebox b { font-size: 20px; display: block; font-variant-numeric: tabular-nums; }
.timebox span { font-size: 11px; color: var(--ink-3); }

.prog { height: 6px; border-radius: 99px; background: var(--border); overflow: hidden; margin: 10px 16px 0; }
.prog i { display: block; height: 100%; background: var(--accent); border-radius: 99px; }

.check { display: flex; align-items: center; gap: 11px; padding: 11px 16px; border-bottom: 1px solid var(--border); font-size: 14px; }
.check:last-child { border-bottom: none; }
.check label { flex: 1; min-width: 0; cursor: pointer; }
.check input[type="checkbox"] { width: 19px; height: 19px; accent-color: var(--accent); flex-shrink: 0; }
.check .sub { display: block; font-size: 12px; color: var(--ink-3); }
.check .ph { margin-left: auto; flex-shrink: 0; }
.check .ctl { display: flex; gap: 2px; flex-shrink: 0; opacity: 0.55; }
@media (max-width: 820px) { .check .ctl { gap: 6px; } }
.check:hover .ctl { opacity: 1; }
.iconbtn { border: none; background: none; color: var(--ink-3); cursor: pointer; padding: 4px; border-radius: 6px; display: inline-flex; }
.iconbtn:hover { background: var(--surface-2); color: var(--ink); }
/* .grab is gone from the checklist (drag-and-drop never fired from touch, so
   the handle did nothing on the only device a cleaner uses); the rule stays
   for any remaining mouse-drag affordance. */
.grab { cursor: grab; color: var(--border-strong); }
.check .ctl .iconbtn[disabled] { opacity: 0.3; cursor: default; }
.addrow { display: flex; gap: 9px; padding: 11px 16px; align-items: center; }
.addrow input { flex: 1; font-family: var(--font); font-size: 13.5px; color: var(--ink); border: 1px dashed var(--border-strong); border-radius: 8px; padding: 8px 11px; background: none; }

.supplies { display: flex; gap: 8px; flex-wrap: wrap; padding: 14px 16px; }
.sup { border: 1px solid var(--border-strong); background: var(--surface); border-radius: 999px; padding: 5px 13px; font-size: 12.5px; cursor: pointer; color: var(--ink-2); font-family: var(--font); }
.sup[aria-pressed="true"] { background: var(--warn-soft); border-color: var(--warn); color: var(--warn); }
.issue-form { border-top: 1px solid var(--border); padding: 14px 16px; display: none; }
.issue-form.open { display: block; }

.toggle { width: 38px; height: 22px; border-radius: 99px; background: var(--border-strong); position: relative; border: none; cursor: pointer; flex-shrink: 0; }
.toggle::after { content: ""; position: absolute; top: 2px; left: 2px; width: 18px; height: 18px; border-radius: 50%; background: #fff; transition: left 0.15s; }
.toggle[aria-checked="true"] { background: var(--accent); }
.toggle[aria-checked="true"]::after { left: 18px; }

.dow-row { display: flex; gap: 8px; flex-wrap: wrap; padding: 4px 0 0; }
.dow { width: 44px; height: 36px; border-radius: 9px; border: 1px solid var(--border-strong); background: var(--surface); font-family: var(--font); font-size: 12.5px; font-weight: 600; color: var(--ink-2); cursor: pointer; }
.dow[aria-pressed="true"] { background: var(--accent-soft); border-color: var(--accent); color: var(--accent-ink); }

.setrow { display: flex; align-items: center; gap: 12px; padding: 12px 16px; border-bottom: 1px solid var(--border); }
.setrow:last-child { border-bottom: none; }
.setrow .s-main { flex: 1; min-width: 0; }
.setrow .s-main b { font-size: 13.5px; font-weight: 600; display: block; }
.setrow .s-main span { font-size: 12px; color: var(--ink-2); }

.frow { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
@media (max-width: 820px) { .frow { grid-template-columns: 1fr; } }

/* Portal preview: an admin looking at the cleaner or owner portal needs to
   see that they are somewhere else, and have one click back. */
.preview-exit {
  display: block; margin: 0 12px 10px; padding: 7px 10px;
  border: 1px solid var(--accent); border-radius: var(--radius-sm);
  background: var(--accent-soft); color: var(--accent-ink);
  font-size: 11.5px; font-weight: 600; text-decoration: none; text-align: center;
}
.preview-exit:hover { text-decoration: underline; }

/* ---------- responsive tables ---------- */
/* Below the mobile breakpoint a data table stops being a grid and becomes a
   list of blocks, each value labelled with its column heading (stamped by
   app.js's labelTableCells). This removes the horizontal scroller that used
   to hide the money columns off the right edge of a phone. */
@media (max-width: 820px) {
  .table-scroll, .scrollcard { overflow-x: visible; }
  /* A capped, internally-scrolling card inside a page that already scrolls is
     two scrollbars competing for the same thumb. On a phone the card just
     grows and the page carries it. */
  .scrollcard { max-height: none; overflow-y: visible; }
  /* A card head is a title plus its actions. On a phone they stop fitting on
     one line, and not wrapping pushed the whole page sideways. */
  .card-head { flex-wrap: wrap; row-gap: 8px; }
  .card-head > .spacer { flex-basis: 100%; height: 0; }
  /* display:none rather than a clipped off-screen block: the headings are
     already copied onto each cell as data-label, so nothing needs them in
     the accessibility tree here, and a clipped thead still reports full-width
     rects that make overflow auditing lie. */
  .table thead, .table tr.head-row { display: none; }
  /* th is included because a table can use row-header th cells inside
     tbody rather than a thead; stacking only td left those in table layout
     and they held the old width (found on the cleaner statements page, which
     the admin sweep could never have reached). */
  /* tfoot too: left in table layout it rendered as a narrow misaligned
     island under the stacked rows — 155px against 341px — holding the
     one figure people actually quote. */
  .table, .table tbody, .table tfoot, .table tr, .table td, .table tbody th { display: block; width: auto; }
  .table tbody th { padding: 6px 0 2px; text-align: left; font-size: 12px; color: var(--ink); border-bottom: 0; }
  .table tr {
    border-bottom: 1px solid var(--border);
    padding: 10px 14px;
  }
  .table tr:last-child { border-bottom: 0; }
  .table td {
    border-bottom: 0;
    padding: 3px 0;
    display: flex; align-items: baseline; gap: 10px;
    /* The value takes whatever the label leaves, and is allowed to wrap
       rather than being ellipsised into uselessness at this width. */
    min-width: 0;
  }
  .table td > * { min-width: 0; }
  .table td .stay-link, .table td a { overflow-wrap: anywhere; }
  /* The heading sits with its value rather than in a column header the
     reader can no longer see. */
  .table td[data-label]::before {
    content: attr(data-label);
    flex: 0 0 92px;
    font-size: 11px; text-transform: uppercase; letter-spacing: 0.4px;
    color: var(--ink-3); font-weight: 600; line-height: 1.5;
  }
  /* A cell with no label is a spanning row: a section heading or a total. */
  .table td[colspan] { padding: 6px 0; }
  .table td.num { text-align: left; justify-content: flex-start; }
  .table td:empty { display: none; }
  /* Actions wrap under their row instead of forcing it wide. */
  .table td .btn.sm { margin-top: 2px; }
  /* Any value in a stacked cell can be an id, a code or a URL with no space
     in it. Previously only links wrapped, so a sync receipt id forced its
     card into a sideways scroller. */
  .table td { overflow-wrap: anywhere; }
  /* Headerless summary tables: the cells already say what they are, so they
     flow as one wrapped block per row rather than a four-line stack. */
  .table.no-head tr { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; }
  .table.no-head td { padding: 0; display: inline-flex; }
  .table.no-head td.num { margin-left: auto; }
  .table.no-head td[colspan] { flex-basis: 100%; }
}

/* A page whose whole surface is the viewport (the calendar's month grid and
   timeline both scroll inside it). 100dvh rather than 100vh because a phone's
   browser chrome is not part of the visible viewport, and minus the fixed
   bottom bar, which was covering the last row of days. */
.cal-main { display: flex; flex-direction: column; height: 100dvh; overflow: hidden; }
@media (max-width: 820px) {
  .cal-main { height: calc(100dvh - 56px); }
}

/* A topbar search field sized for a desktop row. On a phone it takes the row
   it is on rather than sitting at 260px with dead space beside it. */
.topbar-search { width: 260px; max-width: 100%; }
@media (max-width: 820px) { .topbar-search { width: 100%; flex: 1 1 100%; } }

/* .spacer pushes what follows to the far end on a desktop, and at ≤820px the
   responsive-tables block turns it into a line break inside a card head. An
   inline flex:1 on the element defeated that (it also sets flex-basis:0), so
   the designed break never happened - hence the class. */
.spacer { flex-grow: 1; }

/* Two side-by-side columns comparing the two sides of a double booking. At
   375px each track floors at the min-content of a booking code plus a guest
   name, so the card ran past the viewport on the app's most urgent screen. */
@media (max-width: 820px) {
  .diff .diff-body { grid-template-columns: 1fr; }
  .diff .diff-col + .diff-col { border-left: 0; border-top: 1px solid var(--border); }
  .diff .diff-actions .btn { flex: 1 1 100%; }
}

/* The "More" sheet behind the bottom bar's last tab: every destination the
   bar has no room for, permission-filtered and section-labelled exactly like
   the sidebar it stands in for. Without it, seven admin pages had no
   navigation on a phone at all. */
#mobilenav button {
  flex: 1; display: flex; flex-direction: column; align-items: center; gap: 3px;
  padding: 8px 4px 10px; background: none; border: 0; cursor: pointer;
  color: var(--sidebar-ink); font-family: var(--font); font-size: 10px; font-weight: 500;
}
#mobilenav button svg { width: 20px; height: 20px; stroke-width: 1.8; }
#mobilenav button.active { color: #fff; }
#mobilenav button.active svg { color: var(--accent); }

#mobilenav-sheet {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 46;
  background: var(--surface); border-top: 1px solid var(--border);
  border-radius: 16px 16px 0 0; box-shadow: 0 -8px 40px rgba(0, 0, 0, 0.35);
  max-height: 76dvh; display: none; flex-direction: column;
  padding-bottom: env(safe-area-inset-bottom);
}
#mobilenav-sheet.open { display: flex; }
#mobilenav-sheet .ms-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 14px 16px; border-bottom: 1px solid var(--border); flex-shrink: 0;
}
#mobilenav-sheet .ms-body { overflow-y: auto; padding: 8px 10px 14px; min-height: 0; }
#mobilenav-sheet .ms-item {
  display: flex; align-items: center; gap: 11px; min-height: 46px; padding: 0 10px;
  border-radius: 9px; color: var(--ink); text-decoration: none; font-size: 14px; font-weight: 500;
}
#mobilenav-sheet .ms-item svg { width: 19px; height: 19px; stroke-width: 1.7; color: var(--ink-3); }
#mobilenav-sheet .ms-item.active { background: var(--accent-soft); color: var(--accent-ink); }
#mobilenav-sheet .ms-item.active svg { color: var(--accent-ink); }
#mobilenav-sheet .nav-label { padding: 12px 10px 4px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--ink-3); font-weight: 600; }

/* period.js renders this inside the period menu; it had no rule anywhere, so
   the only way to reach a specific month on a phone was a bare label jammed
   against the menu's padding. */
.menu-entry { padding: 8px 10px 4px; display: block; }
.menu-entry label { display: block; font-size: 11px; color: var(--ink-3); font-weight: 600; margin-bottom: 4px; }
.menu-entry .input { width: 100%; }

/* .hint was only ever styled inside a card head, so the same class in a
   topbar rendered at full size and full ink — as loud as the page title. */
.hint { color: var(--ink-3); font-size: 12px; }

/* The assistant transcript. min-height was a floor, so .convo-body's flex:1
   never produced a scroller: the card grew with the conversation forever, the
   composer started below the fold on first paint and moved further down with
   every answer, and chat.scrollTop = chat.scrollHeight was a no-op. A bound
   gives the transcript somewhere to scroll and pins the composer. */
.chat-card { display: flex; flex-direction: column; height: min(600px, calc(100dvh - 210px)); }
@media (max-width: 820px) { .chat-card { height: calc(100dvh - 190px); } }

/* Notification-rule recipient fields. These were inline 12.5px, which no
   media query could reach and which makes iOS zoom the page on focus. */
.recip-field { font-size: 12.5px; padding: 6px 9px; }

/* Evidence upload queue: one row per file with a real progress bar, and the
   failures kept on screen with a Retry rather than cleared. */
.evi-actions { display: flex; gap: 10px; align-items: stretch; flex-wrap: wrap; }
.evi-actions .evi-dropzone { flex: 1 1 160px; }
.evi-queue { display: flex; flex-direction: column; gap: 6px; margin-top: 10px; }
.evi-queue:empty { display: none; }
.evi-q { display: flex; align-items: center; gap: 10px; font-size: 12.5px; color: var(--ink-2); }
.evi-q-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.evi-q-bar { flex: 0 0 96px; height: 5px; border-radius: 999px; background: var(--border); overflow: hidden; }
/* scaleX rather than width: this fires on every progress event of every
   file in a batch, and animating width relayouts the row each time. The
   fill is always full width and is scaled from the left. */
.evi-q-bar i { display: block; width: 100%; height: 100%; background: var(--accent);
  transform-origin: left center; transform: scaleX(0); transition: transform 0.15s linear; }
@media (prefers-reduced-motion: reduce) { .evi-q-bar i { transition: none; } }
.evi-q.done .evi-q-bar i { background: var(--ok); }
.evi-q.failed .evi-q-why { color: var(--danger); font-size: 11.5px; }

/* Show/hide inside a password field (assets/reveal-password.js). The wrapper
   is inserted around the existing input, so nothing about the surrounding form
   markup had to change on the three pages that use it. */
.pw-reveal-wrap { position: relative; display: block; }
.pw-reveal-wrap > input { width: 100%; padding-right: 44px; box-sizing: border-box; }
.pw-reveal {
  position: absolute; right: 4px; top: 50%; transform: translateY(-50%);
  display: inline-flex; align-items: center; justify-content: center;
  width: 36px; height: 36px; border: 0; background: none; border-radius: 8px;
  color: var(--ink-3); cursor: pointer;
}
.pw-reveal:hover { color: var(--ink); background: var(--surface-2); }
/* The auth pages put a 10px bottom margin on their inputs; the wrapper takes
   it so the fields keep their rhythm. */
.si-card .pw-reveal-wrap, .pw-card .pw-reveal-wrap { margin-bottom: 10px; }
.si-card .pw-reveal-wrap > input, .pw-card .pw-reveal-wrap > input { margin-bottom: 0; }
@media (max-width: 820px) { .pw-reveal { width: 40px; height: 40px; } }

.menu-note { padding: 7px 10px; font-size: 12px; color: var(--ink-3); }

/* Inline rename (window.editInPlace), in place of a native prompt(). */
.edit-inline { display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0; flex-wrap: wrap; }
.edit-inline .input { flex: 1 1 140px; min-width: 0; }

/* Print sheet: a document rendered into the page and shown only to the
   printer. Replaces window.open + print(), which mobile Safari and in-app
   browsers block - taking the cleaner's own pay record with it. */
#print-sheet { display: none; }
@media print {
  /* Everything except the sheet leaves the page: the app shell, the nav, the
     cards behind it. Hiding by class would need every page to remember.
     Guarded on the sheet actually having content, so a reader who just hits
     Ctrl+P on an ordinary page still prints the page and not a blank one. */
  body:has(#print-sheet:not(:empty)) > *:not(#print-sheet) { display: none !important; }
  #print-sheet {
    display: block; font-family: Georgia, "Times New Roman", serif; color: #222;
    max-width: 720px; margin: 0 auto; padding: 0;
  }
  #print-sheet h1 { font-size: 20px; margin: 0 0 4px; }
  #print-sheet .sub { color: #666; font-size: 13px; margin-bottom: 20px; }
  #print-sheet table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
  #print-sheet th, #print-sheet td { padding: 7px 9px; border-bottom: 1px solid #ddd; text-align: left; }
  #print-sheet th.num, #print-sheet td.num { text-align: right; }
  #print-sheet .totals { max-width: 320px; margin-left: auto; margin-top: 14px; }
  #print-sheet .totals td { padding: 4px 0; border: none; text-align: right; }
  #print-sheet .totals tr.net td { font-weight: 700; border-top: 1px solid #222; padding-top: 8px; }
  @page { margin: 16mm; }
}

/* Message action menu: one kebab, shown on hover on a desktop and reached by
   long press or right-click everywhere. The bubble is position:relative so the
   button sits in its corner rather than in the text flow. */
.msg { position: relative; }
.msg-menu-btn {
  position: absolute; top: 4px; right: 4px; z-index: 1;
  width: 24px; height: 24px; padding: 0; border: 0; border-radius: 6px; cursor: pointer;
  background: rgba(0, 0, 0, 0.18); color: inherit; opacity: 0;
  display: inline-flex; align-items: center; justify-content: center;
  transition: opacity 0.12s ease;
}
.msg:hover .msg-menu-btn, .msg-menu-btn:focus-visible { opacity: 0.85; }
.msg-menu-btn:hover { opacity: 1; background: rgba(0, 0, 0, 0.3); }
.msg-menu { position: fixed; z-index: 80; }
@media (hover: none) {
  /* No hover to reveal it, and a permanent button on every bubble is litter -
     long press is the affordance here, the same as every messaging app. */
  .msg-menu-btn { display: none; }
}

/* Conversation media, in the context pane. */
.gal-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 5px; margin-bottom: 8px; }
.gal-thumb {
  padding: 0; border: 0; cursor: pointer; aspect-ratio: 1; border-radius: 8px;
  overflow: hidden; background: var(--surface-2);
}
.gal-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.gal-thumb:hover { outline: 2px solid var(--accent); outline-offset: -2px; }
.gal-more {
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 13px; font-weight: 600; color: var(--ink-2); font-family: var(--font);
}
.gal-file {
  display: block; font-size: 12.5px; color: var(--accent-ink); text-decoration: none;
  padding: 5px 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.gal-file:hover { text-decoration: underline; }

/* Evidence thumbnail with its own delete control. Deleting lives on the
   thumbnail, not inside the viewer: it is destructive and specific, and the
   viewer is a place for looking. */
.evi-cell { position: relative; display: inline-flex; }
.evi-del {
  position: absolute; top: -6px; right: -6px; width: 22px; height: 22px; padding: 0;
  border: 1px solid var(--border); border-radius: 50%; cursor: pointer;
  background: var(--surface); color: var(--ink-2);
  display: inline-flex; align-items: center; justify-content: center;
}
.evi-del:hover { background: var(--danger-soft); color: var(--danger); border-color: var(--danger); }
@media (max-width: 820px) { .evi-del { width: 28px; height: 28px; } }

/* Seasonal curve editor: twelve months at a glance, so the biggest lever on a
   nightly price is something you can see and type into rather than a number
   only an AI proposal could change. */
.season-grid { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 8px; }
.season-cell { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.season-cell span { font-size: 11px; color: var(--ink-3); font-weight: 600; text-transform: uppercase; letter-spacing: 0.4px; }
.season-cell .input { width: 100%; }
@media (max-width: 820px) { .season-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

/* The guest-facing price on a calendar day, shown beside the base whenever the
   two differ. Quieter than the base rate: the base is the number the operator
   edits, this is the consequence of it. */
.cv2-guest { display: block; font-size: 10.5px; color: var(--ink-3); font-variant-numeric: tabular-nums; }
.cv2-guest::before { content: "→ "; opacity: 0.7; }

/* ---------- image viewer (window.openViewer) ----------------------------- */
.viewer {
  position: fixed; inset: 0; z-index: 90; display: none;
  background: rgba(10, 11, 10, 0.92);
  align-items: center; justify-content: center;
  opacity: 0; transition: opacity 0.18s ease;
}
.viewer.open { display: flex; opacity: 1; }
.viewer-stage {
  max-width: 100vw; max-height: 100dvh; padding: 64px 64px 84px;
  display: flex; align-items: center; justify-content: center;
}
.viewer-stage img {
  max-width: 100%; max-height: calc(100dvh - 148px);
  border-radius: 10px; box-shadow: 0 24px 80px rgba(0, 0, 0, 0.5);
  /* object-fit is deliberately absent: a photo reporting damage is evidence,
     and cropping evidence to fit a box is how the broken part ends up outside
     the frame. */
}
.viewer-stage img.in-r { animation: viewer-in-r 0.22s ease; }
.viewer-stage img.in-l { animation: viewer-in-l 0.22s ease; }
@keyframes viewer-in-r { from { opacity: 0; transform: translateX(28px); } to { opacity: 1; transform: none; } }
@keyframes viewer-in-l { from { opacity: 0; transform: translateX(-28px); } to { opacity: 1; transform: none; } }

.viewer-bar {
  position: absolute; top: 0; left: 0; right: 0; z-index: 2;
  display: flex; align-items: center; gap: 10px; padding: 14px 16px;
  color: rgba(255, 255, 255, 0.92); font-size: 13px;
  background: linear-gradient(rgba(0, 0, 0, 0.45), transparent);
}
.viewer-count { font-variant-numeric: tabular-nums; }
.viewer-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px; border: 0; border-radius: 10px; cursor: pointer;
  background: rgba(255, 255, 255, 0.12); color: #fff; text-decoration: none;
}
.viewer-btn:hover { background: rgba(255, 255, 255, 0.22); }
.viewer-nav {
  position: absolute; top: 50%; transform: translateY(-50%); z-index: 2;
  width: 48px; height: 48px; border: 0; border-radius: 50%; cursor: pointer;
  background: rgba(255, 255, 255, 0.12); color: #fff;
  display: inline-flex; align-items: center; justify-content: center;
}
.viewer-nav:hover { background: rgba(255, 255, 255, 0.24); }
.viewer-nav[hidden] { display: none; }
.viewer-nav.prev { left: 12px; }
.viewer-nav.next { right: 12px; }
.viewer-cap {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 2;
  padding: 16px 20px calc(20px + env(safe-area-inset-bottom));
  color: rgba(255, 255, 255, 0.86); font-size: 13px; text-align: center;
  background: linear-gradient(transparent, rgba(0, 0, 0, 0.5));
}
.viewer-cap:empty { display: none; }
@media (max-width: 820px) {
  .viewer-stage { padding: 56px 8px 76px; }
  .viewer-nav { width: 44px; height: 44px; }
  .viewer-nav.prev { left: 4px; }
  .viewer-nav.next { right: 4px; }
}
@media (prefers-reduced-motion: reduce) {
  .viewer { transition: none; }
  .viewer-stage img.in-r, .viewer-stage img.in-l { animation: none; }
}

/* ---------- phone ergonomics -------------------------------------------- */
/* Everything below is about a thumb rather than a cursor. The sizes here are
   the shared primitives, so a page does not get to be the exception. */
@media (max-width: 820px) {
  /* 40px is the smallest target a thumb hits reliably. .btn.sm was 25px, the
     kebab 26px, a filter chip 27px and a pager button 24px - every one of
     them a control the phone reader is expected to use constantly. */
  .btn, .btn.sm, .chip, .pager button, .kebab,
  .w-step, .w-period, .iconbtn, .sup, .evi-dropzone, .viewall, .panbtn, .linkish,
  .table .open-cell a, .addrow input, .pager select {
    min-height: 40px; display: inline-flex; align-items: center; justify-content: center;
  }
  .btn.sm, .chip { padding-top: 0; padding-bottom: 0; }
  .kebab, .w-step, .iconbtn, .panbtn { min-width: 40px; justify-content: center; }
  .evi-dropzone { min-height: 48px; }
  /* Hover is the whole affordance for these on a desktop, and a phone has no
     hover: a dimmed icon reads as disabled, a bare word reads as a caption,
     and an underlined figure reads as static text. Each of them changes
     something — a checklist row, the measure a KPI shows, the month a chart
     is on — so on a phone they carry their affordance permanently. */
  .check .ctl { opacity: 1; }
  .kpi .k-label.w-face, .linkish { border-bottom: 1px dashed var(--border-strong); }
  .kpi .k-label.w-face { min-height: 40px; display: inline-flex; align-items: center; }
  /* A cleaner's own week has to look like a week: 7 × 44px fixed cells plus
     gaps came to 356px in 309px, so Sunday wrapped alone onto its own line. */
  .dow-row { display: grid; grid-template-columns: repeat(7, 1fr); }
  .dow { width: auto; min-width: 0; height: 40px; }
  /* A single long word in a property name pushed the clean page to 557px. */
  .hero > * { min-width: 0; }
  .hero h2 { overflow-wrap: anywhere; }
  /* A bare 13px checkbox is the switch behind "email me on a double booking".
     Scaled up rather than replaced, so the existing markup and the .toggle
     primitive elsewhere keep working. */
  input[type="checkbox"], input[type="radio"] { width: 20px; height: 20px; }
  .mrow label, .mrow > input[type="checkbox"] { min-height: 40px; }

  /* A row menu that wraps onto its own line sits well right of centre, and
     right:0 then pushed a 200px menu off the left edge, where nothing can
     scroll to it. */
  .menu { right: auto; left: 0; max-width: calc(100vw - 24px); }
  /* A form control under 16px makes iOS zoom the whole page on focus, and it
     never zooms back out. */
  /* select.input is (0,1,1), so a bare .input rule loses to it - every
     selector here has to be at least as specific as the one it corrects. */
  /* Folded into the header's overflow menu by collapseTopbar. */
  .topbar-secondary[data-folded="1"] { display: none; }
  .input, input.input, select.input, textarea.input, .pager select, .addrow input,
  .si-card input, .pw-card input, .recip-field, input, select, textarea { font-size: 16px; }
}

@media (max-width: 480px) {
  /* Filter chip rows are the one place a 40px floor doubles the height of a
     header, so they keep a slightly tighter target here. */
  .chip-row .chip { min-height: 36px; }
}

/* ---------- scrollbars ----------------------------------------------------
   Every internal scroller was painting a full-width Windows system scrollbar,
   so a page with a few panes read as a stack of grey chrome bars. Thin, in
   the palette, and invisible until the content actually overflows. */
* { scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; }
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 999px; }
::-webkit-scrollbar-thumb:hover { background: var(--ink-3); }
::-webkit-scrollbar-corner { background: transparent; }

/* Desktop keeps all three panes, so the phone's back control has no job.
   Declared before the media query below: same specificity, so source order
   decides, and a later display:none would silently win. */
.inbox-back { display: none; align-items: center; gap: 6px; flex-shrink: 0; }

@media (max-width: 820px) {
  /* --- money flow strip -------------------------------------------------
     Four to six figures side by side is a desktop shape. On a phone it was a
     horizontal scroller, so every figure past the second was off-screen and
     the card grew a system scrollbar. Each figure becomes its own labelled
     block instead: name on the left, amount on the right, note underneath. */
  .flow { display: block; overflow-x: visible; }
  .flow .seg {
    border-right: 0; border-bottom: 1px solid var(--border);
    min-width: 0; padding: 12px 16px;
    display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap;
  }
  .flow .seg:last-child { border-bottom: 0; border-radius: 0 0 var(--radius) var(--radius); }
  .flow .f-label { flex: 1 1 auto; font-size: 12.5px; }
  .flow .f-amt { margin-top: 0; margin-left: auto; font-size: 18px; }
  .flow .f-sub { flex-basis: 100%; margin-top: 2px; }

  /* --- charts -----------------------------------------------------------
     The month window is narrowed in JS at this width (index.html,
     owners.html) so the bars fit; this stops the wrapper reserving desktop
     width and scrolling anyway. Panning is the ‹ › control, not a drag. */
  .chart-scroll { overflow-x: visible; }
  .chart-scroll .months, .chart-scroll .mlabels { min-width: 0; }
  .months .m, .mlabels span { min-width: 0; }

  /* --- inbox: one pane at a time, bound to the viewport -----------------
     Stacking three desktop panes gave the phone a thread list with its own
     300px scrollbar and a conversation with a second one. Removing the
     scrollers instead handed the whole page to the message history, which is
     worse: the header and the composer scroll away and the reader is left
     hunting for the reply box. So the frame is pinned to exactly the room
     left on screen (fitInbox), and inside it precisely one thing scrolls -
     the thread list, or the message history, or the stay details - with the
     conversation header and the composer fixed to the top and bottom. That is
     what a messaging app does, and it is what the desktop three-pane layout
     was already doing with its own height. */
  .inbox { display: block; overflow: hidden; border: 0; background: none; }
  .inbox > .threads, .inbox > .convo, .inbox > .ctx {
    border: 1px solid var(--border); border-radius: var(--radius);
    background: var(--surface); height: 100%; max-height: none;
  }
  .inbox > .convo, .inbox > .ctx { display: none; }
  .inbox.show-convo > .threads { display: none; }
  .inbox.show-convo > .convo { display: flex; }
  .inbox.show-ctx > .threads, .inbox.show-ctx > .convo { display: none; }
  .inbox.show-ctx > .ctx { display: block; }
  /* The one scroller in each view. */
  .inbox > .threads { overflow-y: auto; }
  .inbox > .ctx { overflow-y: auto; }
  .convo-body { overflow-y: auto; }
  /* Filters stay reachable while the list under them scrolls. */
  .threads .t-filters { position: sticky; top: 0; z-index: 1; background: var(--surface); }
  .convo-head, .convo-reply { flex-shrink: 0; }
  .msg { max-width: 88%; }
  /* Back, avatar and identity share the first line; badges and the action
     buttons wrap onto a second rather than squeezing the guest's name into a
     one-word-per-line column. */
  .convo-head { flex-wrap: wrap; row-gap: 8px; padding: 12px 14px; }
  .convo-head .convo-who { flex: 1 1 150px; }
  .convo-head .badge, .convo-head .chip { order: 3; }
  .convo-head .btn.sm { order: 4; flex-shrink: 0; }
  /* Back leads the row, the way it does in every phone list-detail view. */
  .convo-head #convo-back { order: -1; }
  .convo-head #convo-details { order: 5; }
  .inbox-back { display: inline-flex; }
}

