/*
 * app.css — the ONE stylesheet. Tokens, reset, components, canvas.
 *
 * DESIGN BRIEF, in the user's own words: ideas pile up unlisted and it causes
 * "headache, panic, fear of failure, fear of lacking behind". The app must
 * reduce that feeling rather than mirror it back.
 *
 * Three rules follow from that and they are not negotiable:
 *
 *   1. NO RED. Nothing on this canvas is coloured to alarm. A project going
 *      cold FADES and COOLS instead — it recedes rather than shouting. The
 *      information is identical; the emotional register is not.
 *   2. NO COUNTERS OF FAILURE. No "7 overdue" pill anywhere.
 *   3. SPACE. The canvas is mostly empty on purpose. Emptiness is the feature
 *      the user asked for when they said "not suffocating".
 *
 * ONE FILE ON PURPOSE. The first cut split canvas.css from a page stylesheet
 * and immediately had two copies of the token block. Tarly's notes are full of
 * what that costs: a legacy stylesheet per page, each with its own :root and
 * reset, and a rule that only holds on some pages. There is one writer for
 * these tokens and it is this file.
 *
 * Canvas-only rules are scoped to `body.canvas-page`, because the canvas needs
 * `overflow: hidden` and a document page needs to scroll.
 */

:root {
  /* Tells the browser which palette to paint native UI with: scrollbars,
     form controls, and the rubber-band gutter. Without it a dark page keeps
     light scrollbars and a white overscroll flash on macOS. */
  color-scheme: light dark;

  /* The DESK, not paper. It was #F5F2EC, two per cent away from the cards
     sitting on it: a card separated from its canvas by 1.10:1, where 1.0 is
     literally invisible. That is why light mode read as "way too light, can't
     read, poor visibility" — there was no surface, just white on white. */
  --bd-ground:    #E8E0D2;
  --bd-grid:      rgba(27, 26, 22, .06);
  --bd-grid-big:  rgba(27, 26, 22, .095);

  /* The paper. Unchanged: it was never the problem. */
  --bd-card:      #FFFDF9;

  --bd-ink:       #1B1A16;
  --bd-soft:      #56534A;
  /* Was #8B8577, which failed WCAG AA at every size it is used. Solved
     against BOTH the card and the darker canvas, since it appears on both. */
  --bd-faint:     #686459;
  --bd-rule:      #D2C7B2;
  --bd-chrome:    rgba(255, 253, 249, .92);
  --bd-shadow:    rgba(27, 26, 22, .12);
  /* The three layers a real shadow has: the contact line right under the
     edge, the shadow proper, and the wide soft cast on the ground. */
  --bd-lip:       rgba(27, 26, 22, .10);
  --bd-cast:      rgba(27, 26, 22, .07);

  /* Accents carry the stage labels at 10.5px. Every one clears 4.5:1 on the
     card AND on the canvas; slate and plum already did and are untouched. */
  --bd-brass:  #835D1D;
  --bd-patina: #3B6D60;
  --bd-clay:   #995037;
  --bd-slate:  #4A5A6B;
  --bd-plum:   #6E4462;
  --bd-moss:   #5A6A36;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
  color-scheme: dark;

  /* Lifted off near-black. #131519 sits at luminance 0.007, and a near-black
     ground under .45 shadows is what "the overall theme feels heavy" is. */
  --bd-ground:   #16181D;
  --bd-grid:     rgba(233, 229, 220, .05);
  --bd-grid-big: rgba(233, 229, 220, .08);
  --bd-card:     #242931;

  --bd-ink:      #E9E5DC;
  --bd-soft:     #A29C90;
  --bd-faint:    #948E85;
  --bd-rule:     #343A45;
  --bd-chrome:   rgba(36, 41, 49, .92);
  /* .45 was doing the heavy-feeling as much as the ground was. */
  --bd-shadow:   rgba(0, 0, 0, .32);
  --bd-lip:      rgba(0, 0, 0, .46);
  --bd-cast:     rgba(0, 0, 0, .26);

  --bd-brass:  #D9A94F;
  --bd-patina: #6FAA96;
  --bd-clay:   #D08363;
  --bd-slate:  #8AA0B8;
  --bd-plum:   #B486A6;
  --bd-moss:   #9DB169;
  }
}

:root[data-theme="dark"] {
  color-scheme: dark;

  /* Lifted off near-black. #131519 sits at luminance 0.007, and a near-black
     ground under .45 shadows is what "the overall theme feels heavy" is. */
  --bd-ground:   #16181D;
  --bd-grid:     rgba(233, 229, 220, .05);
  --bd-grid-big: rgba(233, 229, 220, .08);
  --bd-card:     #242931;

  --bd-ink:      #E9E5DC;
  --bd-soft:     #A29C90;
  --bd-faint:    #948E85;
  --bd-rule:     #343A45;
  --bd-chrome:   rgba(36, 41, 49, .92);
  /* .45 was doing the heavy-feeling as much as the ground was. */
  --bd-shadow:   rgba(0, 0, 0, .32);
  --bd-lip:      rgba(0, 0, 0, .46);
  --bd-cast:     rgba(0, 0, 0, .26);

  --bd-brass:  #D9A94F;
  --bd-patina: #6FAA96;
  --bd-clay:   #D08363;
  --bd-slate:  #8AA0B8;
  --bd-plum:   #B486A6;
  --bd-moss:   #9DB169;
}

/* ── hidden means hidden ────────────────────────────────────────────
   The browser hides `[hidden]` with a UA rule of `display: none`, which ANY
   explicit display in this file outranks. `.bd-sec { display: flex }` was
   therefore quietly overruling `sec.hidden = true`, and the Next step section
   stayed on screen after it had been told to go — reported as "i checked it on
   Hanzla fiverr account card... still didnt do it".

   Same shape as `.card.is-going` being declared twice, a later flat rule
   silently beating a state. This makes the attribute win everywhere, once. */
[hidden] { display: none !important; }

* { box-sizing: border-box; }

html, body {
  margin: 0;
  background: var(--bd-ground);
  color: var(--bd-ink);
  font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  -webkit-font-smoothing: antialiased;
}

/* Only the canvas locks the document. A page that scrolls must be allowed to. */
html:has(body.canvas-page), body.canvas-page {
  height: 100%;
  overflow: hidden;
  overscroll-behavior: none;     /* no rubber-band / pull-to-refresh while panning */
}

/* ── viewport ─────────────────────────────────────────────────────
   The window onto the world. Fills the screen and clips; all motion
   happens on .world inside it. */
.viewport {
  position: fixed;
  inset: 0;
  overflow: hidden;
  cursor: grab;
  touch-action: none;            /* we handle pan and pinch ourselves */
  /* The grid is drawn on the VIEWPORT, not the world, and its
     background-position is driven from JS. Scaling a 20px grid on a
     transformed element makes it disappear at low zoom and turn into
     stripes at high zoom. */
  background-image:
    linear-gradient(var(--bd-grid) 1px, transparent 1px),
    linear-gradient(90deg, var(--bd-grid) 1px, transparent 1px),
    linear-gradient(var(--bd-grid-big) 1px, transparent 1px),
    linear-gradient(90deg, var(--bd-grid-big) 1px, transparent 1px);
}
.viewport.is-panning { cursor: grabbing; }

/* ── world ────────────────────────────────────────────────────────
   Infinite in every direction including negative. Only transform
   changes, never layout, so panning never triggers reflow. */
.world {
  position: absolute;
  top: 0;
  left: 0;
  transform-origin: 0 0;
  will-change: transform;
}

/* ── card ─────────────────────────────────────────────────────────
   Positioned in world coordinates. */
.card {
  position: absolute;
  background: var(--bd-card);
  /* ── no outline ring ──────────────────────────────────────────────
     This used to be `1px solid var(--bd-rule)` over a single flat shadow,
     which drew each card as an OUTLINED RECTANGLE — reported as "the card
     looks weird in canvas, like the right outline color, I dont get it".
     The ring had no meaning: it wasn't the stage, wasn't the accent, wasn't
     staleness. It was just a line, and a line with nothing to say is noise.

     A card is a piece of paper lying on a desk, and paper has no outline —
     it has an EDGE, which you see because of the shadow under it. So the
     border is gone and the elevation does the separating: a tight contact
     shadow for the edge, a mid one for the lift, a wide soft one for the
     ground. Same language as the notepad panel he liked. */
  border: 1px solid transparent;
  border-radius: 10px;
  padding: 14px 15px 12px;
  box-shadow:
    0 1px 1px var(--bd-lip),
    0 2px 5px var(--bd-shadow),
    0 8px 20px var(--bd-cast);
  display: flex;
  flex-direction: column;
  gap: 7px;
  cursor: grab;
  /* Set from JS per card: 0 warm, 1 cold. Everything that expresses
     staleness reads from this one number. */
  --cool: 0;
  /* Going cold RECEDES. It does not turn red and it does not flash. */
  opacity: calc(1 - var(--cool) * 0.42);
  transition: opacity .25s ease, box-shadow .18s ease, transform .18s ease;
}
.card:hover {
  opacity: 1;                    /* looking at it brings it fully back */
  box-shadow:
    0 1px 1px var(--bd-lip),
    0 4px 10px var(--bd-shadow),
    0 14px 30px var(--bd-cast);
  z-index: 5;
}
.card.is-dragging {
  cursor: grabbing;
  opacity: 1;
  box-shadow:
    0 2px 3px var(--bd-lip),
    0 10px 22px var(--bd-shadow),
    0 26px 50px var(--bd-cast);
  z-index: 20;
}
.card:focus-visible {
  outline: 2px solid var(--bd-brass);
  outline-offset: 3px;
  opacity: 1;
}

/* ── the edge, at night ────────────────────────────────────────────
   A shadow on a dark ground is a dark smudge on dark: it carries almost no
   edge. Real paper under a lamp doesn't lose its edge in a dim room — the
   TOP catches the light. So in the dark the card gets a one-pixel inner
   highlight along its top instead of an outline. Same job, no ring. */
:root[data-theme="dark"] .card,
:root:not([data-theme="light"]) .card {
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .05),
    0 1px 1px var(--bd-lip),
    0 3px 8px var(--bd-shadow),
    0 10px 24px var(--bd-cast);
}
:root[data-theme="dark"] .card:hover,
:root:not([data-theme="light"]) .card:hover {
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .07),
    0 2px 3px var(--bd-lip),
    0 8px 18px var(--bd-shadow),
    0 18px 38px var(--bd-cast);
}
:root[data-theme="dark"] .card.is-dragging,
:root:not([data-theme="light"]) .card.is-dragging {
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .09),
    0 3px 5px var(--bd-lip),
    0 16px 30px var(--bd-shadow),
    0 34px 64px var(--bd-cast);
}

/* The accent stripe is the one saturated thing on a card. */
.card::before {
  content: "";
  position: absolute;
  left: 0;
  top: 12px;
  bottom: 12px;
  width: 3px;
  border-radius: 0 3px 3px 0;
  background: var(--accent);
  opacity: calc(1 - var(--cool) * 0.5);
}

.card.a-brass  { --accent: var(--bd-brass); }
.card.a-patina { --accent: var(--bd-patina); }
.card.a-clay   { --accent: var(--bd-clay); }
.card.a-slate  { --accent: var(--bd-slate); }
.card.a-plum   { --accent: var(--bd-plum); }
.card.a-moss   { --accent: var(--bd-moss); }

.card-name {
  font-size: 15px;
  font-weight: 600;
  line-height: 1.25;
  letter-spacing: -.008em;
  margin: 0;
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

.card-meta {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 11px;
  color: var(--bd-faint);
  margin-top: auto;
}

.stage {
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--accent);
}

/* Days since last touched. Uses the SAME muted colour at every value.
   A number that turns red at 90 days is a tool telling you off. */
.since {
  font-variant-numeric: tabular-nums;
  margin-left: auto;
}

/* ── the next action, or its absence ──────────────────────────────
   A project with no next action is stalled, and that is the most useful
   signal in the app. It is stated plainly and quietly, never in red. */
.next {
  font-size: 12px;
  line-height: 1.4;
  color: var(--bd-soft);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.next.none {
  color: var(--bd-faint);
  font-style: italic;
}

/* ── one line of history ───────────────────────────────────────────
   The rest of what was finished folds into the count in the meta line; this
   is the most recent one, kept because "I want to see the history of what
   was done, its a good feeling". Quieter than the open steps above it — it
   is a reassurance, not a thing to do, and it must not compete with the list
   of what is left. */
.card-last {
  display: flex;
  align-items: baseline;
  gap: 6px;
  font-size: 10.5px;
  line-height: 1.5;
  color: var(--bd-faint);
  overflow: hidden;
  white-space: nowrap;
}
.card-last-t {
  overflow: hidden;
  text-overflow: ellipsis;
  /* Struck through, the same way a finished step reads inside the panel, so
     "done" looks like one thing everywhere in the app. */
  text-decoration: line-through;
  text-decoration-color: var(--bd-rule);
}
.card-tick.done { background: var(--accent); border-color: var(--accent); }

/* A hairline progress rule, only when steps exist. */
.steps {
  height: 2px;
  border-radius: 2px;
  background: var(--bd-rule);
  overflow: hidden;
}
.steps > i {
  display: block;
  height: 100%;
  background: var(--accent);
  border-radius: 2px;
  opacity: .75;
}

/* ── chrome ───────────────────────────────────────────────────────
   Deliberately tiny. A toolbar that fills the top of the screen is the
   suffocating shape the brief rules out. */
.bar {
  position: fixed;
  top: 14px;
  left: 14px;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 8px;
  background: var(--bd-chrome);
  backdrop-filter: blur(14px) saturate(1.5);
  border: 1px solid var(--bd-rule);
  border-radius: 10px;
  box-shadow: 0 2px 10px var(--bd-shadow);
  z-index: 40;
}
.bar-name {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: -.01em;
  padding: 0 8px 0 4px;
}
.bar-sep { width: 1px; height: 20px; background: var(--bd-rule); }

.btn {
  font: inherit;
  font-size: 12.5px;
  font-weight: 500;
  color: var(--bd-ink);
  background: transparent;
  border: 1px solid transparent;
  border-radius: 7px;
  padding: 6px 10px;
  cursor: pointer;
  white-space: nowrap;
}
.btn:hover { background: var(--bd-rule); }
.btn:focus-visible { outline: 2px solid var(--bd-brass); outline-offset: 1px; }
.btn-primary { color: var(--bd-card); background: var(--bd-brass); }
.btn-primary:hover { background: var(--bd-brass); filter: brightness(1.08); }

.zoomer {
  position: fixed;
  bottom: 14px;
  left: 14px;
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 4px;
  background: var(--bd-chrome);
  backdrop-filter: blur(14px) saturate(1.5);
  border: 1px solid var(--bd-rule);
  border-radius: 10px;
  z-index: 40;
}
.zoomer .btn { padding: 5px 9px; }
.zoom-level {
  font-size: 11.5px;
  font-variant-numeric: tabular-nums;
  color: var(--bd-faint);
  min-width: 44px;
  text-align: center;
}

/* ── empty state ──────────────────────────────────────────────────
   The first thing a new user reads. It sets the tone for the whole app,
   so it explains the gesture and then gets out of the way. */
.blank {
  position: fixed;
  inset: 0;
  display: grid;
  place-content: center;
  text-align: center;
  gap: 10px;
  pointer-events: none;
  color: var(--bd-faint);
  padding: 2rem;
}
.blank h1 {
  font-size: 19px;
  font-weight: 600;
  color: var(--bd-soft);
  margin: 0;
  letter-spacing: -.01em;
}
.blank p { font-size: 13.5px; margin: 0; max-width: 30rem; line-height: 1.55; }

/* ── dialog ───────────────────────────────────────────────────────
   A panel that must OCCLUDE cannot be built from translucency alone: on a
   real iPhone the backdrop-filter never arrives and the content behind
   shows through. Opaque base, blur as enhancement. */
dialog.sheet {
  border: 1px solid var(--bd-rule);
  border-radius: 14px;
  background: var(--bd-card);
  color: var(--bd-ink);
  padding: 0;
  width: min(30rem, calc(100vw - 2rem));
  box-shadow: 0 24px 60px var(--bd-shadow);
}
dialog.sheet::backdrop { background: rgba(0, 0, 0, .38); }
.sheet-body { padding: 20px 22px 18px; display: flex; flex-direction: column; gap: 14px; }
.sheet h2 { font-size: 16px; font-weight: 600; margin: 0; letter-spacing: -.01em; }
.sheet-foot {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  padding: 13px 22px;
  border-top: 1px solid var(--bd-rule);
}

.f { display: flex; flex-direction: column; gap: 5px; }
.f > label { font-size: 11.5px; font-weight: 600; color: var(--bd-soft); letter-spacing: .02em; }
.f > input, .f > select, .f > textarea {
  font: inherit;
  /* 16px, or iOS zooms the page on focus and leaves you zoomed. This is a
     documented Tarly bug; the rule is not optional on a touch device. */
  font-size: 16px;
  color: var(--bd-ink);
  background: var(--bd-ground);
  border: 1px solid var(--bd-rule);
  border-radius: 8px;
  padding: 9px 11px;
  min-height: 44px;
}
.f > textarea { min-height: 76px; resize: vertical; line-height: 1.5; }
.f > input:focus, .f > select:focus, .f > textarea:focus {
  outline: 2px solid var(--bd-brass);
  outline-offset: -1px;
  border-color: transparent;
}
.f-hint { font-size: 11.5px; color: var(--bd-faint); line-height: 1.45; }

.swatches { display: flex; gap: 7px; flex-wrap: wrap; }
.swatch {
  width: 30px;
  height: 30px;
  min-height: 30px;
  border-radius: 50%;
  border: 2px solid transparent;
  cursor: pointer;
  padding: 0;
  background: var(--sw);
}
.swatch:focus-visible { outline: 2px solid var(--bd-ink); outline-offset: 2px; }
.swatch[aria-pressed="true"] { border-color: var(--bd-ink); }
.sw-brass  { --sw: var(--bd-brass); }
.sw-patina { --sw: var(--bd-patina); }
.sw-clay   { --sw: var(--bd-clay); }
.sw-slate  { --sw: var(--bd-slate); }
.sw-plum   { --sw: var(--bd-plum); }
.sw-moss   { --sw: var(--bd-moss); }

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

@media (max-width: 640px) {
  .bar {
    left: 10px;
    right: 10px;
    top: 10px;
    /* SCROLL, do not clip. The bar grew to ten items and on a 375px phone
       Team, Account and Sign out were cut off past the right edge with no
       way to reach them — you could not sign out on a phone at all.
       Found by looking at the page; nothing automated catches it. */
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    /* A fade on the right edge that disappears at the end of the scroll, so
       it never claims there is more when there is not. */
    background-image: linear-gradient(to left, var(--bd-chrome), transparent 28px);
    background-attachment: local, scroll;
  }
  .bar::-webkit-scrollbar { display: none; }
  /* ── an overflow container CLIPS its absolutely positioned children ──
     The scroll above was added so a ten-item toolbar could be reached on a
     375px phone. It also silently turned every menu inside the bar into a
     clipped one: the popover is `position: absolute` inside `.menu`, and a
     scroll container is its clipping ancestor, so on a phone the account
     menu opened as a 50px box with its contents cut off.

     The RIGHT bar holds one button and can never need to scroll, so it opts
     out entirely and its menu behaves. The left bar genuinely does need the
     scroll, and its two menus are still clipped at this width — that needs
     the popover moved out of the scrolling element, which is a bigger change
     than this one and is written down rather than half-done here. */
  .bar-right { overflow: visible; background-image: none; }
  /* Without this the items compress to nothing instead of overflowing,
     which is a different way of being unreadable. */
  .bar > * { flex: none; }
  .bar-name { display: none; }
  .zoomer { bottom: 10px; left: 10px; }
}

/* ══════════════════════════════════════════════════════════════════
   DOCUMENT PAGES
   Everything below is for pages that scroll: the project detail page,
   the review flow, the list, and sign-in. They share the tokens above,
   which is the whole reason this is one file.
   ══════════════════════════════════════════════════════════════════ */

.page {
  max-width: 54rem;
  margin: 0 auto;
  padding: 0 1.25rem 5rem;
}

/* Top bar on document pages. Static, not fixed: a fixed bar on a scrolling
   page eats vertical space on every screen, and Tarly measured its own
   chrome at 32% of a phone before any content appeared. */
.topbar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 14px 0;
  margin-bottom: 1.5rem;
  border-bottom: 1px solid var(--bd-rule);
}
.topbar .btn { padding: 6px 10px; }
.topbar-spacer { margin-left: auto; }
.back {
  font-size: 13px;
  color: var(--bd-soft);
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 6px 8px;
  border-radius: 7px;
  min-height: 36px;
}
.back:hover { background: var(--bd-rule); color: var(--bd-ink); }

h1.title {
  font-size: 26px;
  font-weight: 600;
  letter-spacing: -.02em;
  line-height: 1.15;
  margin: 0 0 .5rem;
  text-wrap: balance;
}
.sub {
  font-size: 13.5px;
  color: var(--bd-faint);
  margin: 0 0 1.75rem;
  line-height: 1.55;
}

.panel {
  background: var(--bd-card);
  border: 1px solid var(--bd-rule);
  border-radius: 12px;
  padding: 18px 20px;
  margin-bottom: 1rem;
}
.panel > h2 {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--bd-faint);
  margin: 0 0 14px;
}
.panel-row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }

/* ── the pipeline ─────────────────────────────────────────────────
   Four stages as a row of steps. The exits (parked, dropped) are NOT
   in this row; they are separate actions, because they are not further
   along, they are off to the side. */
.pipe { display: flex; gap: 6px; flex-wrap: wrap; }
.pipe-step {
  font: inherit;
  font-size: 12.5px;
  font-weight: 600;
  padding: 9px 14px;
  min-height: 40px;
  border-radius: 8px;
  border: 1px solid var(--bd-rule);
  background: transparent;
  color: var(--bd-soft);
  cursor: pointer;
}
.pipe-step:hover { background: var(--bd-rule); color: var(--bd-ink); }
.pipe-step[aria-current="true"] {
  background: var(--bd-ink);
  border-color: var(--bd-ink);
  color: var(--bd-ground);
}
/* Stages already passed read as done without being loud about it. */
.pipe-step.past { color: var(--bd-ink); border-color: var(--bd-ink); }
.pipe-step:focus-visible { outline: 2px solid var(--bd-brass); outline-offset: 2px; }

/* ── steps list ───────────────────────────────────────────────────
   Thin on purpose. No assignee, no priority, no drag handles. */
.step-row {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 9px 0;
  border-top: 1px solid var(--bd-rule);
}
.step-row:first-child { border-top: none; }
.step-check {
  appearance: none;
  width: 19px;
  height: 19px;
  flex: none;
  border: 1.5px solid var(--bd-faint);
  border-radius: 5px;
  cursor: pointer;
  position: relative;
  background: transparent;
}
.step-check:checked { background: var(--bd-patina); border-color: var(--bd-patina); }
.step-check:checked::after {
  content: "";
  position: absolute;
  left: 6px; top: 2px;
  width: 5px; height: 10px;
  border: solid var(--bd-card);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}
/* The wrapping label is the 44px target, not the 19px box. A checkbox
   stretched to 44px square looks broken; a label that size does not. */
.step-label {
  display: flex;
  align-items: center;
  gap: 11px;
  min-height: 44px;
  flex: 1;
  cursor: pointer;
  /* Same reason as .bd-step-label: a pasted link has nowhere to break. */
  min-width: 0;
  overflow-wrap: anywhere;
  font-size: 14px;
  line-height: 1.45;
}
.step-label.done { color: var(--bd-faint); text-decoration: line-through; }
.step-kill {
  font: inherit;
  font-size: 15px;
  line-height: 1;
  color: var(--bd-faint);
  background: transparent;
  border: 0;
  border-radius: 7px;
  cursor: pointer;
  min-width: 34px;
  min-height: 34px;
}
.step-kill:hover { background: var(--bd-rule); color: var(--bd-ink); }

.inline-form { display: flex; gap: 8px; margin-top: 10px; }
.inline-form input {
  flex: 1;
  font: inherit;
  font-size: 16px;      /* 16px or iOS zooms on focus */
  min-height: 44px;
  color: var(--bd-ink);
  background: var(--bd-ground);
  border: 1px solid var(--bd-rule);
  border-radius: 8px;
  padding: 9px 11px;
}
.inline-form input:focus { outline: 2px solid var(--bd-brass); outline-offset: -1px; }

/* ── the timeline ─────────────────────────────────────────────────
   The honest history. Events that were not progress are dimmed rather
   than hidden: "I reviewed this and left it parked" is a real thing to
   have done and deserves to be visible. */
.event {
  display: flex;
  gap: 12px;
  padding: 8px 0;
  font-size: 13.5px;
  line-height: 1.5;
  border-top: 1px solid var(--bd-rule);
}
.event:first-child { border-top: none; }
.event.quiet { color: var(--bd-faint); }
.event-when {
  flex: none;
  width: 5.5rem;
  color: var(--bd-faint);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  padding-top: 1px;
}
.event-what { flex: 1; }
.event-kind { font-weight: 600; }

/* ── list / portfolio ─────────────────────────────────────────────── */
.rows { display: flex; flex-direction: column; }
.row-item {
  display: grid;
  grid-template-columns: 1fr auto auto;
  gap: 14px;
  align-items: center;
  padding: 13px 0;
  border-top: 1px solid var(--bd-rule);
  text-decoration: none;
  color: inherit;
}
.row-item:first-child { border-top: none; }
.row-item:hover .row-name { color: var(--bd-brass); }
.row-main { min-width: 0; }
/* display:block matters. These are <span>s inside a <span>, so without it
   they render INLINE and the project name runs straight into its subtitle
   with no break — which is exactly what shipped on /list and /patterns
   before anyone looked at the page. check-classes proves a class resolves,
   not that the layout is right. */
.row-name {
  display: block;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: -.008em;
}
.row-next {
  display: block;
  font-size: 12.5px;
  color: var(--bd-soft);
  margin-top: 2px;
}
.row-next.none { color: var(--bd-faint); font-style: italic; }
.row-stage {
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--bd-faint);
}
.row-days {
  font-size: 12.5px;
  color: var(--bd-faint);
  font-variant-numeric: tabular-nums;
  min-width: 3.6rem;
  text-align: right;
}
/* Dropped projects are kept and readable, never hidden and never red.
   The graveyard is a feature; the decision is the value. */
.row-item.gone .row-name { color: var(--bd-faint); }

/* ── review ───────────────────────────────────────────────────────
   Three doors, equal weight. Dropping must never look like losing:
   a tool that shames you is a tool you stop opening, and the streak
   research is explicit that perfectionism is what makes people quit. */
.doors { display: grid; grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr)); gap: 10px; }
.door {
  display: flex;
  flex-direction: column;
  gap: 6px;
  text-align: left;
  font: inherit;
  padding: 15px 16px;
  border: 1px solid var(--bd-rule);
  border-radius: 10px;
  background: var(--bd-card);
  color: inherit;
  cursor: pointer;
}
.door:hover { border-color: var(--bd-ink); }
.door:focus-visible { outline: 2px solid var(--bd-brass); outline-offset: 2px; }
.door-title { font-size: 14.5px; font-weight: 600; }
.door-blurb { font-size: 12.5px; color: var(--bd-soft); line-height: 1.45; }

.empty {
  text-align: center;
  padding: 4rem 1rem;
  color: var(--bd-faint);
}
.empty h2 { font-size: 17px; font-weight: 600; color: var(--bd-soft); margin: 0 0 .5rem; }
.empty p { font-size: 13.5px; margin: 0 auto; max-width: 28rem; line-height: 1.55; }

.flash {
  background: var(--bd-card);
  border: 1px solid var(--bd-rule);
  border-left: 3px solid var(--bd-brass);
  border-radius: 8px;
  padding: 11px 14px;
  font-size: 13.5px;
  margin-bottom: 1rem;
}

.danger { color: var(--bd-clay); }

@media (max-width: 640px) {
  .page { padding: 0 1rem 4rem; }
  h1.title { font-size: 22px; }
  .row-item { grid-template-columns: 1fr auto; }
  .row-stage { display: none; }
  .event-when { width: 4.2rem; }
}

/* ── share links ──────────────────────────────────────────────────── */
.token {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12.5px;
  word-break: break-all;
  background: var(--bd-ground);
  border: 1px solid var(--bd-rule);
  border-radius: 7px;
  padding: 9px 11px;
  margin: 9px 0;
  user-select: all;          /* one click selects the whole link */
}
.check-line {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 40px;
  font-size: 14px;
  cursor: pointer;
}
.ro-note {
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--bd-faint);
  padding: 0 8px;
}
/* A read-only card must not look draggable. */
.card.ro { cursor: default; }

/* ── sign in with Google ──────────────────────────────────────────── */
.google-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 46px;
  border: 1px solid var(--bd-rule);
  background: var(--bd-card);
  font-size: 14.5px;
  font-weight: 600;
  text-decoration: none;
  color: var(--bd-ink);
}
.google-btn:hover { border-color: var(--bd-ink); background: var(--bd-card); }
.or {
  display: flex;
  align-items: center;
  gap: 12px;
  font-size: 11.5px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--bd-faint);
}
.or::before, .or::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--bd-rule);
}

/* ── security page ────────────────────────────────────────────────── */
.qr-holder {
  display: flex;
  justify-content: center;
  padding: 14px;
  background: #fff;              /* a scanner needs light behind the code */
  border-radius: 10px;
}
.codes {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: 8px;
}
.code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 14px;
  letter-spacing: .06em;
  background: var(--bd-ground);
  border: 1px solid var(--bd-rule);
  border-radius: 7px;
  padding: 9px 11px;
  text-align: center;
  user-select: all;
}
.inline-input {
  flex: 1;
  min-width: 12rem;
  font: inherit;
  font-size: 16px;               /* 16px or iOS zooms on focus */
  min-height: 44px;
  color: var(--bd-ink);
  background: var(--bd-ground);
  border: 1px solid var(--bd-rule);
  border-radius: 8px;
  padding: 9px 11px;
}
.inline-input:focus { outline: 2px solid var(--bd-brass); outline-offset: -1px; }

/* ── patterns ─────────────────────────────────────────────────────── */
.pattern-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 10px;
}
.pattern-head h2 {
  font-size: 15px;
  font-weight: 600;
  letter-spacing: -.01em;
  text-transform: none;   /* panel h2 is an uppercase label; this is a heading */
  color: var(--bd-ink);
}
.pattern-count {
  font-size: 12.5px;
  color: var(--bd-faint);
  font-variant-numeric: tabular-nums;
  flex: none;
}
/* Deliberately the accent, never a warning colour. This is a record of your
   own decisions, not an alarm about them. */
.bar-track {
  height: 6px;
  border-radius: 3px;
  background: var(--bd-rule);
  overflow: hidden;
}
.bar-track > i {
  display: block;
  height: 100%;
  border-radius: 3px;
  background: var(--bd-brass);
  opacity: .8;
}


/* ══ ICON MOTION ═══════════════════════════════════════════════════════
   The first attempt lifted the whole glyph on hover and was rejected in
   these words: "by animations, i did not meant icon lift, i meant the icon
   animation... gear animation to move gears".

   The distinction is the whole point. Lifting an icon is motion applied TO
   a drawing. What was asked for is motion that belongs to the drawing —
   a gear turning its own teeth. So nothing here translates a glyph. Every
   rule below moves one PART of one icon, and the motion is always that part
   doing the job the icon depicts: sliders slide, bars grow off a baseline,
   a tick gets drawn, someone walks out of a door.

   Three rules held throughout:
     · transform and opacity only, so this is compositor work and a hover
       never costs a layout. `transform-box: fill-box` is what makes
       transform-origin mean the part's own box instead of the SVG's.
     · the icon must READ THE SAME at rest. An icon whose resting state is
       mid-animation is a broken icon, so every part's default is identity.
     · it stops for `prefers-reduced-motion`. Stated once, at the end. */

.ic * { transform-box: fill-box; transform-origin: center; }
.ic .i-pv, .ic .i-ph, .ic .i-rule, .ic .i-bar,
.ic .i-dot, .ic .i-knob, .ic .i-half, .ic .i-ray,
.ic .i-disc, .ic .i-crescent, .ic .i-tick, .ic .i-ring,
.ic .i-flap, .ic .i-out, .ic .i-mate, .ic .i-head,
.ic .i-screen, .ic .i-span, .ic .i-row, .ic .i-spark, .ic .i-tile,
.ic .i-snap, .ic .i-sheet {
  transition: transform .34s cubic-bezier(.2, .8, .25, 1),
              opacity   .28s ease,
              stroke-dashoffset .4s cubic-bezier(.2, .8, .25, 1);
}

/* One hover surface for all of them. `.tool` is the toolbar, `.pop-i` the
   menu rows, `.tile` the settings cards — an icon animates wherever it is
   sitting on something you can actually press. */
.tool:hover .ic, .tool:focus-visible .ic,
.pop-i:hover .ic, .pop-i:focus-visible .ic,
.tile:hover .ic, .tile:focus-visible .ic { /* group hook, no styles of its own */ }

/* Plus — the strokes draw out from the middle. */
.tool:hover .i-pv, .tool:focus-visible .i-pv,
.pop-i:hover .i-pv, .pop-i:focus-visible .i-pv { transform: scaleY(.55); }
.tool:hover .i-ph, .tool:focus-visible .i-ph,
.pop-i:hover .i-ph, .pop-i:focus-visible .i-ph { transform: scaleX(.55); transition-delay: .05s; }

/* Revoke — the link comes apart, and the bar between the halves goes with
   it. The exact inverse of IconLink pulling together: the same drawing saying
   the opposite thing, so a link and its undo read as one idea. */
.pop-i:hover .i-u1, .pop-i:focus-visible .i-u1,
.lnk-row:hover .i-u1 { transform: translateX(-2.2px); }
.pop-i:hover .i-u2, .pop-i:focus-visible .i-u2,
.lnk-row:hover .i-u2 { transform: translateX(2.2px); }
.pop-i:hover .i-snap, .pop-i:focus-visible .i-snap,
.lnk-row:hover .i-snap { transform: scaleX(.2); opacity: .35; }

/* Copy — the top sheet lifts, the way you would peel one off a pile. */
.pop-i:hover .i-sheet, .pop-i:focus-visible .i-sheet,
.btn:hover .i-sheet { transform: translate(1.2px, -1.4px); }

/* Tidy — two tiles sit out of line at rest and settle into the grid. The
   icon performs the action, which is the rule the whole set follows. */
.ic .i-t2 { transform: translate(1.6px, -1.2px) rotate(4deg); }
.ic .i-t3 { transform: translate(-1.4px, 1.1px) rotate(-3deg); }
.pop-i:hover .i-t2, .pop-i:focus-visible .i-t2,
.pop-i:hover .i-t3, .pop-i:focus-visible .i-t3 { transform: none; }

/* Chit — the spark turns a little and swells, and its two sparks follow. */
.tool:hover .i-spark, .tool:focus-visible .i-spark { transform: rotate(18deg) scale(1.08); }

/* Focus / Views — rules settle in from the left, longest first. Origin is
   the left edge because that is where a written line starts. */
.ic .i-rule { transform-origin: left center; }
.tool:hover .i-r1, .tool:focus-visible .i-r1,
.pop-i:hover .i-r1, .pop-i:focus-visible .i-r1 { transform: scaleX(.62); }
.tool:hover .i-r2, .tool:focus-visible .i-r2,
.pop-i:hover .i-r2, .pop-i:focus-visible .i-r2 { transform: scaleX(.7); transition-delay: .05s; }
.tool:hover .i-r3, .tool:focus-visible .i-r3,
.pop-i:hover .i-r3, .pop-i:focus-visible .i-r3 { transform: scaleX(.78); transition-delay: .1s; }

/* Review / Check — the tick is drawn rather than revealed. A dash the
   length of the path, offset by the same, is a line that hasn't been drawn
   yet; releasing the offset draws it. */
.ic .i-tick { stroke-dasharray: 22; stroke-dashoffset: 0; }
.tool:hover .i-tick, .tool:focus-visible .i-tick,
.pop-i:hover .i-tick, .pop-i:focus-visible .i-tick {
  animation: ic-draw .5s cubic-bezier(.3, .75, .3, 1);
}
@keyframes ic-draw { from { stroke-dashoffset: 22; } to { stroke-dashoffset: 0; } }
.tool:hover .i-ring, .tool:focus-visible .i-ring { transform: scale(.93); }

/* List — the bullets step in ahead of their rows. */
.tool:hover .i-d1, .tool:focus-visible .i-d1 { transform: translateX(2.2px); }
.tool:hover .i-d2, .tool:focus-visible .i-d2 { transform: translateX(2.2px); transition-delay: .06s; }
.tool:hover .i-d3, .tool:focus-visible .i-d3 { transform: translateX(2.2px); transition-delay: .12s; }

/* Patterns — bars grow off the baseline, in order. Origin is the BOTTOM,
   which is the only origin a bar chart has. */
.ic .i-bar { transform-origin: center bottom; }
.tool:hover .i-b1, .tool:focus-visible .i-b1 { transform: scaleY(.45); }
.tool:hover .i-b2, .tool:focus-visible .i-b2 { transform: scaleY(.45); transition-delay: .05s; }
.tool:hover .i-b3, .tool:focus-visible .i-b3 { transform: scaleY(.45); transition-delay: .1s; }
.tool:hover .i-b4, .tool:focus-visible .i-b4 { transform: scaleY(.45); transition-delay: .15s; }

/* Share / Link — the two halves ease apart along the diagonal they sit on,
   then come back when you leave. A link is a thing that JOINS, so showing
   the join is showing the meaning. */
.tool:hover .i-h1, .tool:focus-visible .i-h1,
.pop-i:hover .i-h1, .pop-i:focus-visible .i-h1 { transform: translate(1.5px, -1.5px); }
.tool:hover .i-h2, .tool:focus-visible .i-h2,
.pop-i:hover .i-h2, .pop-i:focus-visible .i-h2 { transform: translate(-1.5px, 1.5px); }
.pop-i:hover .i-span, .pop-i:focus-visible .i-span { transform: scaleX(.62); }

/* Mail — the flap lifts. */
.ic .i-flap { transform-origin: center top; }
.pop-i:hover .i-flap, .pop-i:focus-visible .i-flap { transform: scaleY(-.62); }

/* Settings — each knob runs its own track and settles. The tracks are the
   two long rules, so the knobs move only along X, only within their own
   segment: a knob leaving its track would be a broken control. */
.tool:hover .i-k1, .tool:focus-visible .i-k1,
.pop-i:hover .i-k1, .pop-i:focus-visible .i-k1 { transform: translateX(-3.5px); }
.tool:hover .i-k2, .tool:focus-visible .i-k2,
.pop-i:hover .i-k2, .pop-i:focus-visible .i-k2 { transform: translateX(4px); transition-delay: .07s; }

/* Account / Team — the head nods, the second person leans in. */
.pop-i:hover .i-head, .pop-i:focus-visible .i-head { transform: translateY(-.9px); }
.pop-i:hover .i-mate, .pop-i:focus-visible .i-mate { transform: translateX(-1.6px); }

/* Sign out — someone walks out. It comes back, because a hover is not a
   commitment and an icon that empties itself on hover reads as already done. */
.pop-i:hover .i-out, .pop-i:focus-visible .i-out { transform: translateX(2px); }

/* Theme — the sun's rays turn around the disc, the moon rocks, the screen
   wakes. The disc itself does NOT turn: a circle turning is invisible work. */
.pop-i:hover .i-ray, .pop-i:focus-visible .i-ray { transform: rotate(45deg); }
.pop-i:hover .i-disc, .pop-i:focus-visible .i-disc { transform: scale(1.1); }
.pop-i:hover .i-crescent, .pop-i:focus-visible .i-crescent { transform: rotate(-14deg); }
.pop-i:hover .i-screen, .pop-i:focus-visible .i-screen { transform: scale(1.06); }

/* An icon that keeps moving while someone is trying to read past it is a
   distraction, and this app's whole brief is the opposite of distraction. */
@media (prefers-reduced-motion: reduce) {
  .ic *, .ic .i-tick { transition: none !important; animation: none !important; }
  .tool:hover .ic *, .pop-i:hover .ic *, .tile:hover .ic * { transform: none !important; }
}


/* ══ FIND ═════════════════════════════════════════════════════════════
   Top centre rather than top right: a search field is the thing you are
   looking at while you use it, and the two toolbars already own the corners.
   It slides down from behind the top edge, so it reads as having been
   summoned rather than as having always been there and never noticed. */
.find {
  position: fixed;
  top: 14px;
  left: 50%;
  z-index: 60;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 8px 7px 11px;
  min-width: 300px;
  background: var(--bd-chrome);
  backdrop-filter: blur(14px) saturate(1.5);
  border: 1px solid var(--bd-rule);
  border-radius: 10px;
  box-shadow: 0 2px 10px var(--bd-shadow), 0 12px 30px var(--bd-cast);
  /* Off by default, and OUT OF THE TAB ORDER while off — a hidden field you
     can still tab into is a trap that has no visible cause. */
  transform: translate(-50%, -140%);
  opacity: 0;
  pointer-events: none;
  visibility: hidden;
  transition: transform .28s cubic-bezier(.2, .8, .25, 1), opacity .2s ease,
              visibility 0s linear .28s;
}
.find.on {
  transform: translate(-50%, 0);
  opacity: 1;
  pointer-events: auto;
  visibility: visible;
  transition: transform .3s cubic-bezier(.2, .8, .25, 1), opacity .18s ease,
              visibility 0s;
}
.find-i { color: var(--bd-faint); flex: none; }
.find-in {
  flex: 1 1 auto;
  min-width: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: 13.5px;
  color: var(--bd-ink);
  outline: none;
  padding: 0;
}
.find-in::placeholder { color: var(--bd-faint); }
.find-n {
  flex: none;
  font-size: 11.5px;
  color: var(--bd-faint);
  font-variant-numeric: tabular-nums;
  min-width: 3.6em;
  text-align: right;
}
/* No red for a miss. Nothing on this canvas is coloured to alarm, and
   "you typed something that isn't here" is the least alarming fact there is. */
.find-n.none { font-style: italic; }
.find-x {
  flex: none;
  border: 0;
  background: none;
  color: var(--bd-faint);
  font-size: 17px;
  line-height: 1;
  padding: 2px 5px;
  border-radius: 6px;
  cursor: pointer;
}
.find-x:hover { color: var(--bd-ink); background: var(--bd-rule); }

/* ── what a search does to the canvas ──────────────────────────────
   Cards that miss RECEDE, they are not removed. See the note in canvas.js:
   deleting them would keep the layout (they are absolutely positioned) but
   destroy your sense of where you are on your own board. */
.card.is-dim {
  opacity: .18;
  filter: saturate(.35);
  transition: opacity .22s ease, filter .22s ease;
}
.card.is-hit { opacity: 1; }
/* The current match gets the same ring the keyboard focus gets, because it
   is the same idea: this is the one you are on. */
.card.is-cur {
  outline: 2px solid var(--bd-brass);
  outline-offset: 3px;
  z-index: 6;
}
@media (prefers-reduced-motion: reduce) {
  .find { transition: opacity .01s linear; }
}



/* ── dragging a card is a drag, not a text selection ───────────────
   Without this, a press and move over a card name leaves a trail of
   highlighted words behind it. The drag itself works perfectly, which is
   why it reads as a rendering fault rather than a missing rule, and why it
   survives being looked at.

   Set on the BODY for the length of the gesture, so it cannot be stranded
   on a card that is removed mid-drag.

   ── THIS RULE WAS HERE, AND WAS DELETED BY ACCIDENT ────────────────
   It sat directly beneath the particle-layer styles. When `fx.js` and its
   CSS were removed, this went with the block it happened to be adjacent to
   — it had nothing to do with particles. Nothing caught it: the class is
   added from JavaScript, so check-classes.mjs (which reads MARKUP) has
   never been able to see it, and the app looked and behaved correctly in
   every way except this one. It came back as "I am still seeing that text
   selection when I am dragging cards", three weeks after it was fixed.

   tools/check-js-classes.mjs now closes that blind spot. */
body.is-grabbing,
body.is-grabbing * {
  user-select: none;
  -webkit-user-select: none;
}

/* ══ THE DOT ══════════════════════════════════════════════════════════
   "custom cursor like a dot, just like we have on alwaztahir.com".

   A solid dot that tracks exactly and a ring that lags behind it. Only
   transform and opacity move, so the whole thing is compositor work and a
   pointer sweep never costs a layout.

   The RING CARRIES THE CURSOR STATES. That is the part the marketing site
   did not need: `cursor: none` throws away grab, grabbing and resize, all of
   which mean something on a canvas, so the ring says them instead. See the
   note at the top of cursor.js. */
/* The dot and ring live in here so they can join the TOP LAYER as a popover
   and paint above a modal <dialog>. A popover carries UA defaults that are all
   wrong for a full-screen pass-through layer: a border, padding, a background,
   `inset: 0` with `margin: auto`, and `width: fit-content`. Every one of them
   is undone here.

   `pointer-events: none` is load-bearing: this sits over the entire viewport,
   and without it nothing underneath could be clicked at all. */
.cur-layer,
.cur-layer:popover-open {
  position: fixed;
  inset: 0;
  margin: 0;
  padding: 0;
  border: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  max-height: none;
  background: transparent;
  overflow: visible;
  pointer-events: none;
  display: block;
}
/* No dimming behind it, or the page would grey out behind the cursor. */
.cur-layer::backdrop { background: transparent; }

.cur, .cur-ring {
  position: fixed;
  top: 0;
  left: 0;
  pointer-events: none;
  z-index: 9999;
  opacity: 0;
  will-change: transform;
}
.cur {
  width: 6px;
  height: 6px;
  margin: -3px 0 0 -3px;
  background: var(--bd-ink);
  border-radius: 50%;
  /* ── NEVER TRANSITION THE DOT'S TRANSFORM ──────────────────────────
     This said `transform .18s ease` and it made the whole thing feel
     broken: every pointer movement was ANIMATED over 180ms, so the dot was
     not tracking the pointer at all, it was easing toward it, permanently
     behind and permanently arriving. Reported as "it feels kinda slow.. i
     kind of dont have control over it just like i have with a cursor".

     The dot IS the pointer. It must be exactly where the pointer is, on the
     same frame, with nothing smoothing it. The ring is the part that lags,
     and it lags by arithmetic in the rAF loop, not by a transition — which
     is why the ring can carry a transition on its size and colour and the
     dot cannot carry one on its position.

     The reference this was copied from has no transition on the dot at all.
     This one was added by copying the ring's rule without asking what
     `transform` means on each. */
  transition: opacity .2s ease, background .18s ease;
}
.cur-ring {
  width: 30px;
  height: 30px;
  margin: -15px 0 0 -15px;
  border: 1px solid var(--bd-faint);
  border-radius: 50%;
  transition: width .2s ease, height .2s ease, margin .2s ease, opacity .2s ease,
              border-color .18s ease, background .18s ease, border-radius .18s ease;
}

/* Over something pressable it opens and fills faintly, exactly as the
   original does. */
.cur-ring.on-press {
  width: 46px;
  height: 46px;
  margin: -23px 0 0 -23px;
  border-color: transparent;
  background: color-mix(in srgb, var(--bd-ink) 8%, transparent);
}

/* Over a card it TIGHTENS and turns brass. Closing in is the right direction
   for "you can pick this up" — opening out is for "you can press this", and
   the two must not read the same. */
.cur-ring.on-card {
  width: 22px;
  height: 22px;
  margin: -11px 0 0 -11px;
  border-color: var(--bd-brass);
}

/* Holding it: closed almost to the dot and filled. The grip of a hand. */
.cur-ring.is-hold {
  width: 13px;
  height: 13px;
  margin: -6.5px 0 0 -6.5px;
  border-color: var(--bd-brass);
  background: color-mix(in srgb, var(--bd-brass) 22%, transparent);
}

/* Over the resize grip it squares off, because that corner changes a
   rectangle and a circle is the wrong shape to promise that with. */
.cur-ring.on-grip {
  width: 20px;
  height: 20px;
  margin: -10px 0 0 -10px;
  border-radius: 3px;
  border-color: var(--bd-brass);
}

/* ── over text, the real caret comes back ──────────────────────────
   Not a style choice. The I-beam's position BETWEEN CHARACTERS is
   information no ring can express, and writing is what this app is for. */
.cur-ring.on-text, .cur.on-text { opacity: 0 !important; }
/* ── hiding the real cursor, and why this uses !important ──────────
   The first version listed the elements that set a cursor — a, button,
   .card, .grip — and lost anyway. `.viewport.is-panning { cursor: grabbing }`
   is two classes, and `body.has-cursor` is one class plus an element, so the
   panning rule WINS on specificity and the grab hand came back during
   exactly the gesture that most wants the ring instead. Reported as "i still
   see the mouse drag cursor icon and beneath it, is the new cursor
   animation".

   Listing more selectors is a specificity arms race that the next `cursor:`
   declaration anywhere in this file re-opens, silently, with no test that
   would catch it. There are thirty of them already.

   So this is stated as what it actually is: a MODE. While the dot is on,
   nothing else may set a cursor. That is precisely what !important means and
   it is the honest tool for it — the alternative is a rule that has to be
   revisited every time someone styles a hover.

   The text exception still wins because it is more specific AND important,
   so the two are compared on specificity as normal. */
body.has-cursor,
body.has-cursor * { cursor: none !important; }

/* The one thing that outranks the mode. See cursor.js: an I-beam's position
   between characters is information no ring can express. */
body.has-cursor.cur-text input,
body.has-cursor.cur-text textarea,
body.has-cursor.cur-text [contenteditable] { cursor: text !important; }

/* A thing that chases the pointer forever is precisely what this setting is
   asking not to see. cursor.js also refuses to start, so this is belt and
   braces for a preference changed mid-session. */
@media (prefers-reduced-motion: reduce) {
  .cur, .cur-ring { display: none !important; }
  /* `revert` and not `auto`: the real cursors have to come BACK, which means
     undoing the mode rather than flattening everything to an arrow. Written
     as a wildcard for the same reason the mode is. */
  body.has-cursor, body.has-cursor * { cursor: revert !important; }
}

/* ── the role, under the action it belongs to ──────────────────────
   Not a second menu item. The role is a property of the one thing this menu
   does, and putting it on its own line makes that structural rather than
   something you infer from two near-identical sentences.

   Indented to the text column, not the icon column, so it reads as belonging
   to the item above it rather than as a third item. */
.role {
  display: flex;
  margin: 2px 11px 4px 39px;
  border: 1px solid var(--bd-rule);
  border-radius: 7px;
  overflow: hidden;
}
.role-i {
  flex: 1;
  border: 0;
  background: none;
  font: inherit;
  font-size: 12px;
  color: var(--bd-soft);
  padding: 6px 8px;
  cursor: pointer;
  transition: background .14s ease, color .14s ease;
}
.role-i:hover { color: var(--bd-ink); }
.role-i.on { background: var(--bd-brass); color: var(--bd-card); font-weight: 600; }

/* ── a chosen face ─────────────────────────────────────────────────
   The image fills the button and the button is already a circle, so the art
   is exported square and cropped here rather than baked into a circle. That
   keeps one file usable if a face ever appears somewhere non-circular, such
   as a presence row that decides to be square. */
.avatar.has-face { padding: 0; overflow: hidden; background: none; }
.avatar.has-face img { width: 100%; height: 100%; display: block; object-fit: cover; }

/* The picker. Big enough to judge the character, which is the whole point of
   choosing one, but shown beside a real 34px preview would be better still
   once there are twelve. */
.faces { display: flex; gap: 14px; flex-wrap: wrap; }
.face {
  border: 1px solid var(--bd-rule);
  background: none;
  border-radius: 10px;
  padding: 10px 12px 8px;
  cursor: pointer;
  font: inherit;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 7px;
  min-width: 74px;
  transition: border-color .14s ease, background .14s ease;
}
.face:hover { border-color: var(--bd-soft); }
.face.on { border-color: var(--bd-brass); background: var(--bd-ground); }
.face img,
.face-letter {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  display: block;
  object-fit: cover;
}
.face-letter {
  display: grid;
  place-items: center;
  border: 1px solid var(--bd-rule);
  background: var(--bd-ground);
  color: var(--bd-brass);
  font-size: 17px;
  font-weight: 600;
}
.face i { font-style: normal; font-size: 11.5px; color: var(--bd-faint); }
.face.on i { color: var(--bd-ink); }

/* ══ SHARING ══════════════════════════════════════════════════════════
   The link comes first and the settings sit under it, each one editing the
   link on screen. See the note in share.tsx for why the form it replaced
   produced four live links with three never opened. */
.lnk-row { display: flex; align-items: stretch; }
.lnk-url {
  flex: 1;
  min-width: 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12.5px;
  padding: 11px 12px;
  border: 1px solid var(--bd-brass);
  border-right: 0;
  border-radius: 8px 0 0 8px;
  background: var(--bd-ground);
  color: var(--bd-ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  /* Selectable, because the clipboard can refuse and this is shown once. */
  user-select: all;
}
.lnk-row .btn { border-radius: 0 8px 8px 0; flex: none; }

/* Label beside its control, not above it. Four headed blocks read as four
   sections; this reads as one short list of settings, in about half the
   height. */
.fields {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 12px 16px;
  align-items: center;
  margin-top: 20px;
}
.flab { font-size: 12.5px; color: var(--bd-faint); }
.incl { display: flex; gap: 18px; flex-wrap: wrap; }

.lnk-head { display: flex; align-items: baseline; gap: 10px; }
.lnk-head h2 { flex: 1; }
.lnk-n {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11.5px;
  color: var(--bd-faint);
}
.lnk-item {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 11px 0;
  border-top: 1px solid var(--bd-rule);
}
.lnk-item:first-of-type { border-top: 0; }
/* Filled means somebody has actually opened it. Three of the four links on
   this account had never been touched and no screen said so. */
.lnk-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--bd-brass);
  flex: none;
}
.lnk-dot.cold { background: var(--bd-rule); }
.lnk-body { flex: 1; min-width: 0; }
.lnk-body b {
  display: block;
  font-size: 12.5px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.lnk-body i {
  display: block;
  font-style: normal;
  font-size: 11px;
  color: var(--bd-faint);
  margin-top: 2px;
}
.lnk-item.gone { opacity: .55; }
.lnk-item .btn { display: inline-flex; align-items: center; gap: 6px; }

@media (max-width: 560px) {
  .fields { grid-template-columns: 1fr; gap: 6px 0; }
  .flab { margin-top: 12px; }
}

/* ══ CONNECTORS ═══════════════════════════════════════════════════════ */
.cx-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 13px 0;
  border-top: 1px solid var(--bd-rule);
}
.cx-row:first-of-type { border-top: 0; }
.cx-ico {
  flex: none;
  width: 38px; height: 38px;
  display: grid; place-items: center;
  border-radius: 9px;
  background: var(--bd-ground);
  color: var(--bd-brass);
}
.cx-body { flex: 1 1 auto; min-width: 0; display: block; }
.cx-body b { display: block; font-size: 14px; font-weight: 600; }
.cx-body i {
  display: block;
  font-style: normal;
  font-size: 11.5px;
  color: var(--bd-faint);
  line-height: 1.45;
  margin-top: 2px;
}
.cx-write {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 11.5px;
  color: var(--bd-faint);
  cursor: pointer;
}
/* Listed and not live. Dimmed AND labelled: dimming alone reads as a bug on
   a page where everything else is pressable. */
.cx-row.is-off .cx-ico,
.cx-row.is-off .cx-body { opacity: .5; }
/* Confirming before a security setting is weakened. Stacked rather than
   inline, because the field and the button it authorises belong together and
   two side-by-side forms on one row read as four unrelated controls. */
.step-up { flex-wrap: wrap; gap: 14px; }
.step-up form { display: flex; flex-direction: column; gap: 8px; align-items: stretch; min-width: 200px; }

/* ══════════════════════════════════════════════════════════════════
   THE DOOR — sign in, reset, join
   Centred on the canvas's own grid, because the door should look like the
   room. Everything here is scoped to `.door` so no other page inherits a
   full-height centring layout by accident.
   ══════════════════════════════════════════════════════════════════ */
/* ══════════════════════════════════════════════════════════════════
   THE DESK — the sign-in page

   Not a form in a box. The page IS a desk with unfinished work already lying
   on it, and the sign-in is the card nearest you: the only one square to the
   reader and the only one in focus.

   Every class is prefixed `dk-`. The obvious name for this was `.door`, and
   `.door` is already three buttons on /review — a second rule for it made
   each of them 100vh tall with a grid behind it, live, for about an hour.
   Grep before naming; this file has had that bug before.

   THE PALETTE IS WARM, AND SCOPED. `--bd-ground` and friends are blue-biased
   near-black, which is what every dark SaaS login in the world looks like.
   The tokens below are brown-biased and live only under `.dk`, so the door is
   a warm room you pass through and the workspace behind it is untouched.
   ══════════════════════════════════════════════════════════════════ */
.dk {
  --desk:        #E5DCCB;
  --desk-2:      #DBD0BB;
  --leaf:        #FFFDF9;
  --dk-ink:      #1B1A16;
  --dk-faint:    #6B6659;
  --dk-rule:     #D2C7B2;
  --br:          #8A6222;
  --br-lit:      #C79A45;
  --br-dim:      #5E4113;
  --dk-sh:       rgba(46, 36, 20, .17);
  --dk-sh-soft:  rgba(46, 36, 20, .10);
  --dk-grain:    rgba(90, 70, 40, .05);

  position: relative;
  min-height: 100vh;
  min-height: 100dvh;          /* or a phone's URL bar eats the centring */
  display: grid;
  place-items: center;
  padding: 30px 20px;
  overflow: hidden;
  background:
    radial-gradient(120% 90% at 50% 42%, var(--desk-2) 0%, transparent 62%),
    var(--desk);
  color: var(--dk-ink);
}

/* The room at night. Brown, not blue: the point is a warm room with the lamp
   off, not a control panel. Both the un-stamped system-dark state and an
   explicit choice are covered. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .dk {
    --desk: #191410; --desk-2: #221B15; --leaf: #2E271E;
    --dk-ink: #F0E7D8; --dk-faint: #A2947E; --dk-rule: #3B3128;
    --br: #D9A94F; --br-lit: #F2D394; --br-dim: #8A6222;
    --dk-sh: rgba(0,0,0,.5); --dk-sh-soft: rgba(0,0,0,.34);
    --dk-grain: rgba(255, 228, 176, .035);
  }
}
:root[data-theme="dark"] .dk {
  --desk: #191410; --desk-2: #221B15; --leaf: #2E271E;
  --dk-ink: #F0E7D8; --dk-faint: #A2947E; --dk-rule: #3B3128;
  --br: #D9A94F; --br-lit: #F2D394; --br-dim: #8A6222;
  --dk-sh: rgba(0,0,0,.5); --dk-sh-soft: rgba(0,0,0,.34);
  --dk-grain: rgba(255, 228, 176, .035);
}

/* A fine grain, because a large flat area reads as literally flat. */
.dk::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image:
    repeating-linear-gradient(0deg, var(--dk-grain) 0 1px, transparent 1px 3px),
    repeating-linear-gradient(90deg, var(--dk-grain) 0 1px, transparent 1px 4px);
  opacity: .55;
  pointer-events: none;
}

/* `Page` wraps everything in `.page`: 54rem wide, centred, 80px of bottom
   padding for a document that scrolls. A desk is not a document. Scoped with
   `:has` so this touches only this page and not every `chrome={false}` one. */
.page:has(.dk) { max-width: none; margin: 0; padding: 0; }

/* ── the work already lying there ─────────────────────────────────── */
.dk-strewn { position: absolute; inset: 0; overflow: hidden; }

.dk-leaf {
  position: absolute;
  width: 216px;
  background: var(--leaf);
  border: 1px solid var(--dk-rule);
  border-radius: 10px;
  padding: 13px 14px 11px;
  box-shadow: 0 1px 2px var(--dk-sh-soft), 0 14px 34px var(--dk-sh);
  /* Behind and slightly soft: this is not what you are here to read, it is
     what you are here to get back to. */
  filter: blur(1.4px);
  opacity: .62;
  transform-origin: 50% 50%;
}
.dk-leaf b { display: block; font-size: 13px; font-weight: 600; letter-spacing: -.01em; margin-bottom: 7px; }
.dk-leaf i { display: block; font-style: normal; font-size: 11px; color: var(--dk-faint); line-height: 1.85; }
.dk-leaf s { display: block; text-decoration: none; margin-top: 9px; font-size: 9px; letter-spacing: .13em; text-transform: uppercase; color: var(--br); }
/* The left rule is the stage colour, exactly as on the real canvas. */
.dk-leaf::before {
  content: "";
  position: absolute;
  left: 0; top: 12px; bottom: 12px;
  width: 2px; border-radius: 2px;
  background: var(--br);
}
.dk-sage::before  { background: #7FA88C; }  .dk-sage s  { color: #7FA88C; }
.dk-dusk::before  { background: #8497BC; }  .dk-dusk s  { color: #8497BC; }
.dk-olive::before { background: #93A45E; }  .dk-olive s { color: #93A45E; }

/* ── the card nearest you ─────────────────────────────────────────── */
.dk-hand {
  position: relative;
  z-index: 3;
  width: min(384px, 100%);
  background: var(--leaf);
  border: 1px solid var(--dk-rule);
  border-radius: 14px;
  padding: 34px 32px 26px;
  box-shadow:
    0 2px 4px var(--dk-sh-soft),
    0 30px 70px var(--dk-sh),
    inset 0 1px 0 rgba(255, 255, 255, .05);
}
/* Its own brass edge, wider than the others: this is the live one. */
.dk-hand::before {
  content: "";
  position: absolute;
  left: 0; top: 22px; bottom: 22px;
  width: 3px; border-radius: 3px;
  background: linear-gradient(180deg, var(--br-lit), var(--br) 45%, var(--br-dim));
}

/* Brass as METAL, not as one hex used for buttons: a lit top edge falling to
   a shadowed base. */
.dk-mark {
  font-size: 33px;
  font-weight: 800;
  line-height: 1;
  letter-spacing: -.032em;
  margin: 0 0 9px;
  background: linear-gradient(172deg, var(--br-lit) 4%, var(--br) 46%, var(--br-dim) 96%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
/* `balance` rather than a hand-tuned max-width: the sentence broke after
   "Pick", stranding "it back up." on its own line. Two short lines of even
   length read as one thought; a 3/1 split reads as a mistake. */
.dk-said { margin: 0 0 26px; font-size: 14px; line-height: 1.5; color: var(--dk-faint); max-width: 32ch; text-wrap: balance; }

.dk-oauth {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  width: 100%; min-height: 46px;
  border-radius: 9px;
  border: 1px solid var(--dk-rule);
  background: transparent;
  color: var(--dk-ink);
  font: inherit; font-size: 14.5px;
  cursor: pointer; text-decoration: none;
  transition: border-color .15s ease, background .15s ease;
}
.dk-oauth:hover { border-color: var(--br); background: var(--desk-2); }

.dk-split { display: flex; align-items: center; gap: 12px; margin: 20px 0; font-size: 11.5px; color: var(--dk-faint); }
.dk-split::before, .dk-split::after { content: ""; flex: 1; height: 1px; background: var(--dk-rule); }

.dk-fields { display: flex; flex-direction: column; gap: 15px; }
.dk-fields label { display: flex; flex-direction: column; gap: 6px; }
.dk-fields label > span { font-size: 12px; color: var(--dk-faint); }
/* A ruled line rather than a filled box. A bordered, rounded, filled input is
   the single element that most makes a card read as a form. */
/* 16px, and not for looks: iOS Safari AUTO-ZOOMS any focused field under
   16px, and /signin has no maximum-scale to stop it — so tapping Email
   shifted and magnified the whole page with no way back. The New idea
   sheet and the guest door were already 16; sign-in was the outlier. */
.dk-fields input {
  font: inherit; font-size: 16px;
  color: var(--dk-ink);
  background: transparent;
  border: 0;
  border-bottom: 1.5px solid var(--dk-rule);
  border-radius: 0;
  padding: 8px 2px;
  min-height: 40px;
  transition: border-color .16s ease;
}
.dk-fields input::placeholder { color: var(--dk-faint); opacity: .65; }
.dk-fields input:focus { outline: none; border-bottom-color: var(--br); }

.dk-go {
  width: 100%; min-height: 46px; margin-top: 22px;
  border: 0; border-radius: 9px;
  font: inherit; font-size: 15px; font-weight: 600;
  color: #241A08;
  cursor: pointer; position: relative; overflow: hidden;
  background: linear-gradient(178deg, var(--br-lit) 0%, var(--br) 52%, var(--br-dim) 100%);
  box-shadow: 0 1px 0 rgba(255,255,255,.28) inset, 0 6px 18px rgba(138, 98, 34, .3);
  transition: transform .1s ease;
}
/* A highlight that runs across it. Metal catches the light; flat colour does not. */
.dk-go::after {
  content: "";
  position: absolute; inset: 0;
  background: linear-gradient(105deg, transparent 34%, rgba(255,255,255,.42) 50%, transparent 66%);
  transform: translateX(-120%);
  transition: transform .62s cubic-bezier(.3,.7,.3,1);
}
.dk-go:hover::after { transform: translateX(120%); }
.dk-go:active { transform: translateY(1px); }

.dk-feet {
  display: flex; justify-content: space-between; gap: 12px;
  margin-top: 22px; padding-top: 15px;
  border-top: 1px solid var(--dk-rule);
  font-size: 12.5px;
}
.dk-link {
  color: var(--dk-faint);
  text-decoration: none;
  font: inherit; font-size: 12.5px;
  background: none; border: 0; padding: 0; cursor: pointer;
}
.dk-link:hover { color: var(--br); }
.dk-msg { margin: 12px 0 0; font-size: 12.5px; color: var(--dk-faint); }
.dk-flash {
  margin: 0 0 18px; padding: 10px 12px;
  border-radius: 8px;
  border: 1px solid var(--dk-rule);
  background: var(--desk-2);
  font-size: 13px;
  color: var(--dk-ink);
}
.dk :focus-visible { outline: 2px solid var(--br); outline-offset: 3px; }

/* ── the deal ─────────────────────────────────────────────────────────
   Everything arrives once. The strewn work slides up and settles; the card in
   hand resolves out of a blur, which is the same focus-pull the canvas
   already uses when a card appears. Under a second, then still for good. */
@keyframes dk-settle {
  from { opacity: 0; transform: var(--rest) translateY(24px) scale(.97); }
  to   { opacity: .62; transform: var(--rest); }
}
@keyframes dk-focus {
  from { opacity: 0; transform: translateY(16px) scale(1.035); filter: blur(9px); }
  to   { opacity: 1; transform: none; filter: blur(0); }
}
.dk-leaf { animation: dk-settle .72s cubic-bezier(.19,1,.3,1) both; }
.dk-hand { animation: dk-focus .78s cubic-bezier(.19,1,.3,1) .22s both; }

@media (prefers-reduced-motion: reduce) {
  .dk-leaf, .dk-hand { animation: none; filter: none; }
  .dk-leaf { opacity: .62; transform: var(--rest); filter: blur(1.4px); }
  .dk-hand { opacity: 1; transform: none; }
  .dk-go::after { display: none; }
}

/* Below this the strewn work is noise behind a form, so it goes entirely. */
@media (max-width: 860px) {
  .dk-strewn { display: none; }
  .dk-hand { padding: 28px 24px 22px; }
}

/* An action chit proposed and did not take. Sits under its reply rather than
   replacing it, because the sentence explaining what it wants is the reply. */
.chit-ask {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--bd-rule);
}
.chit-ask-t { flex: 1 1 100%; font-size: 12.5px; color: var(--bd-faint); }
.chit-ask .btn { font-size: 12px; padding: 4px 12px; min-height: 28px; }

/* ── the guest's own controls ──────────────────────────────────────
   Inside the guest menu only. Deliberately plain: a person reading someone
   else's board for ten minutes should not be given a settings page, and the
   two things they might genuinely want to change are their name and how
   bright the screen is. */
.g-me {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 8px;
  align-items: center;
  padding: 4px 12px 10px;
}
.g-name {
  font: inherit;
  font-size: 13px;
  color: var(--bd-ink);
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--bd-rule);
  padding: 5px 0;
  min-width: 0;
}
.g-name:focus { outline: none; border-bottom-color: var(--bd-brass); }
.g-colors {
  grid-column: 1 / -1;
  display: flex;
  gap: 6px;
}
.g-col {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  border: 2px solid transparent;
  padding: 0;
  cursor: pointer;
  background-clip: padding-box;
  /* `.sw-*` only declares the custom property; every element that wants to be
     that colour has to spend it. */
  background-color: var(--sw);
}
/* The ring, not a fill change: the swatch has to keep showing its own colour
   while saying it is the chosen one. */
.g-col.on { border-color: var(--bd-ink); }
.g-save { flex: none; font-size: 12px; padding: 5px 12px; }

/* The ask, which is a small form rather than a menu item now that it takes an
   address. Same grid as the name row above it, so the two read as one column
   of controls rather than two unrelated widgets. */
.g-ask {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 8px 8px;
  align-items: center;
  padding: 8px 12px 12px;
}
.g-ask b { grid-column: 1 / -1; font-size: 13px; font-weight: 600; color: var(--bd-ink); }
.g-said { grid-column: 1 / -1; font-size: 11.5px; color: var(--bd-faint); font-style: normal; }

/* A guest's avatar is their chosen colour, because that colour is already how
   their ticks and their writing are marked on the owner's board. The letter
   sits on it rather than beside it, so the control answers "which of us am I"
   in one glance, the same job the owner's initial does. */
.avatar[class*="sw-"] {
  background: var(--sw);
  color: var(--bd-paper);
  border-color: transparent;
}

/* What the link will cover. A native select so it can never be clipped by
   the menu it lives in, styled down to look like the rest of the sheet
   rather than like a form control dropped onto it. */
.pop-pick {
  font: inherit;
  font-size: 12.5px;
  color: var(--bd-ink);
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--bd-rule);
  border-radius: 0;
  padding: 5px 12px 6px;
  margin: 0 12px 8px;
  width: calc(100% - 24px);
  cursor: pointer;
}
.pop-pick:focus { outline: none; border-bottom-color: var(--bd-brass); }

.cx-soon {
  flex: none;
  font-size: 11px;
  color: var(--bd-faint);
  border: 1px solid var(--bd-rule);
  border-radius: 999px;
  padding: 3px 9px;
}
.cx-on {
  /* A running connector says so in the one colour this app uses for
     something that is working. The dimmed row already separates listed from
     live; this makes it readable at a glance rather than by comparison. */
  color: var(--bd-patina);
  border-color: color-mix(in srgb, var(--bd-patina) 40%, transparent);
}
/* The one-time URL. Monospace because it is a credential to be copied
   exactly, and full-width because a wrapped token invites a partial copy. */
.cx-url {
  width: 100%;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12px;
  padding: 10px 11px;
  border: 1px solid var(--bd-brass);
  border-radius: 8px;
  background: var(--bd-ground);
  color: var(--bd-ink);
}
.cx-new { border-left: 3px solid var(--bd-brass); }
/* The mark sits inside .cx-ico, which already paints it. Declared so the
   class is not a name with nothing behind it — check-classes is right to
   fail on one, because a class that resolves to nothing is usually a typo
   in the class that was meant. */
.cx-mark { display: block; }
/* Quieter than .btn: disconnecting is not the action this page is FOR, and a
   destructive control drawn at the same weight as the primary one gets
   pressed by accident. No red — nothing in this app is coloured to alarm. */
.btn-quiet {
  background: none;
  border: 1px solid var(--bd-rule);
  color: var(--bd-faint);
}
.btn-quiet:hover { color: var(--bd-ink); border-color: var(--bd-soft); }


/* ══ CHIT ═════════════════════════════════════════════════════════════
   Spotlight's shape: one line, centred high, which GROWS downward as the
   conversation gives it something to show. The transcript has no height of
   its own until it has content — a fixed box with an empty scroll area
   inside is the "collapsed panel" look this is deliberately not. */
.chit {
  position: fixed;
  inset: 0;
  z-index: 80;
  display: grid;
  /* High rather than centred. A window that opens across the middle of the
     canvas covers the thing you are asking about. */
  align-content: start;
  justify-items: center;
  padding-top: 14vh;
  background: color-mix(in srgb, var(--bd-ground) 55%, transparent);
  /* 2px, matching the panel's scrim. More blur makes the canvas behind read
     as a different, heavier material. */
  backdrop-filter: blur(2px);
  opacity: 0;
  pointer-events: none;
  visibility: hidden;
  transition: opacity .18s ease, visibility 0s linear .18s;
}
.chit.on {
  opacity: 1;
  pointer-events: auto;
  visibility: visible;
  transition: opacity .16s ease, visibility 0s;
}

.chit > * {
  width: min(620px, calc(100vw - 32px));
  background: var(--bd-card);
  box-shadow: 0 1px 1px var(--bd-lip), 0 8px 24px var(--bd-shadow), 0 28px 60px var(--bd-cast);
}
.chit-tape {
  /* No height at rest. This is what makes it a LINE until it is more. */
  max-height: 0;
  overflow: hidden;
  border-radius: 12px 12px 0 0;
  transition: max-height .3s cubic-bezier(.2, .8, .25, 1), padding .3s ease;
  padding: 0 16px;
}
.chit.has-tape .chit-tape {
  max-height: min(46vh, 420px);
  overflow-y: auto;
  padding: 14px 16px 8px;
  border-bottom: 1px solid var(--bd-rule);
}
.chit-line {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 12px 14px;
  border-radius: 12px;
}
.chit.has-tape .chit-line { border-radius: 0 0 12px 12px; }

.chit-i { flex: none; color: var(--bd-brass); margin-top: 2px; }
.chit-in {
  flex: 1 1 auto;
  min-width: 0;
  border: 0;
  background: none;
  resize: none;
  font: inherit;
  font-size: 14.5px;
  line-height: 1.5;
  color: var(--bd-ink);
  outline: none;
  padding: 0;
  max-height: 140px;
}
.chit-in::placeholder { color: var(--bd-faint); }
.chit-hint { flex: none; color: var(--bd-faint); font-size: 13px; margin-top: 2px; }

.chit-row {
  font-size: 13.5px;
  line-height: 1.55;
  margin-bottom: 10px;
  white-space: pre-wrap;
  word-break: break-word;
}
/* What you said sits quieter than what came back. The answer is the thing
   being read; your own question you already know. */
.chit-you { color: var(--bd-faint); }
.chit-you::before { content: "› "; }
.chit-bot { color: var(--bd-ink); }
.chit-row.is-pending { color: var(--bd-faint); font-style: italic; }
/* No red. An error here is a sentence, not an alarm — same rule as the rest
   of the app. */
.chit-row.is-err { color: var(--bd-soft); font-style: italic; }
/* What it actually touched. An assistant that restages three projects and
   then says "done" is one you stop trusting. */
.chit-used {
  margin-top: 5px;
  font-size: 10.5px;
  letter-spacing: .02em;
  color: var(--bd-faint);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.chit.is-busy .chit-in { opacity: .5; }

@media (pointer: coarse) {
  .chit { padding-top: 8vh; }
  .chit-line { padding: 14px; }
}
@media (prefers-reduced-motion: reduce) {
  .chit-tape { transition: none; }
}

/* ── resize grip ──────────────────────────────────────────────────── */
.grip {
  position: absolute;
  right: 0;
  bottom: 0;
  width: 18px;
  height: 18px;
  cursor: nwse-resize;
  opacity: 0;
  transition: opacity .15s ease;
  /* Two hairlines suggesting a corner, drawn rather than an icon file. */
  background:
    linear-gradient(135deg, transparent 45%, var(--bd-faint) 45%, var(--bd-faint) 55%, transparent 55%),
    linear-gradient(135deg, transparent 70%, var(--bd-faint) 70%, var(--bd-faint) 80%, transparent 80%);
}
/* Revealed on hover only. A grip on every card at all times turns a calm
   canvas into a fidgety one, which is the opposite of the brief. */
.card:hover .grip { opacity: .9; }
.card.is-resizing { opacity: 1; }
.card.is-resizing .grip { opacity: 1; }
/* A read-only viewer must not be shown a control they cannot use. */
.card.ro .grip { display: none; }

/* ── the in-place project panel ────────────────────────────────────
   Clicking a card no longer navigates: the card becomes the editor where
   it sits. See panel.js for the FLIP geometry; this file owns only how it
   looks once it has arrived.

   `position: fixed` is not a detail. The panel must live in SCREEN space,
   because anything inside `.world` inherits the canvas zoom and the editor
   would be unreadable at 30% and absurd at 250%. */
/* A dim, not a frosted sheet of glass. 7px of blur plus a saturate filter
   made the canvas behind read as a separate, heavier material; the panel is
   meant to be a page laid ON your desk, not a window cut through it. */
.bd-scrim {
  position: fixed;
  inset: 0;
  z-index: 40;
  background: color-mix(in srgb, var(--bd-ground) 42%, transparent);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
  animation: bd-fade .26s ease both;
}
.bd-scrim.is-out { animation: bd-fade .2s ease reverse both; }
@keyframes bd-fade { from { opacity: 0; } to { opacity: 1; } }

/* ── the page ──────────────────────────────────────────────────────
   Was a dialog: a border, a 16px radius, a double shadow at 24px and 64px
   of blur, and three horizontal rules chopping it into head, body and foot.
   All of that is chrome describing a container.

   A notepad has no chrome. One soft shadow to lift the paper off the desk,
   and after that it is only type and space. */
.bd-panel {
  position: fixed;
  z-index: 41;
  width: min(680px, 92vw);
  max-height: min(700px, 88vh);
  background: var(--bd-card);
  border: 1px solid var(--bd-rule);
  border-radius: 14px;
  box-shadow: 0 14px 44px var(--bd-shadow);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  will-change: transform, opacity;
}
/* panel.js writes `transform: none` at the end of the open, so the centring
   cannot live in `transform` as well. Position it by offset instead. */
.bd-panel { left: max(4vw, calc(50vw - 380px)); top: max(7vh, calc(50vh - 340px)); transform: none; }

/* The contents fade in a beat after the frame starts moving. That delay is
   what hides the non-uniform scale: for the first ninety milliseconds you
   see a card growing, not stretched text. */
.bd-inner {
  display: flex;
  flex-direction: column;
  min-height: 0;
  opacity: 0;
  transition: opacity .22s ease .09s;
}
.bd-panel.is-in .bd-inner { opacity: 1; }

/* No rule under the title. On a page, a heading is separated by SPACE. */
.bd-head {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 26px 30px 2px;
  flex: none;
}
.bd-title {
  font-size: 23px;
  font-weight: 600;
  letter-spacing: -.018em;
  line-height: 1.25;
  margin: 0;
  flex: 1;
  min-width: 0;
  text-wrap: balance;
}
.bd-x {
  font: inherit;
  font-size: 20px;
  line-height: 1;
  color: var(--bd-faint);
  background: transparent;
  border: 0;
  border-radius: 8px;
  cursor: pointer;
  flex: none;
  min-width: 36px;
  min-height: 36px;
}
.bd-x:hover { color: var(--bd-ink); background: var(--bd-ground); }

.bd-body {
  padding: 12px 30px 20px;
  overflow-y: auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 26px;
}
.bd-sec { display: flex; flex-direction: column; gap: 10px; }
/* Sentence case, not micro-caps. Letter-spaced uppercase is the voice of a
   form or a dashboard; this app writes in plain sentences everywhere else. */
.bd-lab {
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  color: var(--bd-faint);
  margin: 0;
}
.bd-hint { font-size: 11.5px; color: var(--bd-faint); line-height: 1.45; margin: 0; }
.bd-loading { font-style: italic; }

.bd-pipe { display: flex; gap: 6px; flex-wrap: wrap; }
.bd-step {
  font: inherit;
  font-size: 12.5px;
  font-weight: 600;
  padding: 9px 14px;
  min-height: 40px;
  border-radius: 8px;
  border: 1px solid var(--bd-rule);
  background: transparent;
  color: var(--bd-soft);
  cursor: pointer;
  transition: background .14s ease, color .14s ease, border-color .14s ease;
}
.bd-step:hover { border-color: var(--bd-faint); color: var(--bd-ink); }
.bd-step.on { background: var(--bd-ink); border-color: var(--bd-ink); color: var(--bd-card); }

.bd-row { display: flex; gap: 8px; align-items: stretch; }
/* You write ON the page, not into a box. A filled, bordered, rounded input
   sitting on paper is the one element that most makes a panel feel like a
   form. A single rule underneath says "write here" just as clearly. */
.bd-input {
  font: inherit;
  font-size: 15px;
  line-height: 1.55;
  color: var(--bd-ink);
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--bd-rule);
  border-radius: 0;
  padding: 8px 2px;
  flex: 1;
  min-width: 0;
  min-height: 40px;
}
.bd-input:focus { outline: 2px solid var(--bd-brass); outline-offset: 1px; border-color: transparent; }
.bd-save {
  font: inherit;
  font-size: 12.5px;
  font-weight: 500;
  color: var(--bd-card);
  background: var(--bd-brass);
  border: 1px solid transparent;
  border-radius: 8px;
  padding: 0 16px;
  min-height: 42px;
  cursor: pointer;
  flex: none;
}

.bd-steps { display: flex; flex-direction: column; }
.bd-step-row {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: 11px;
  align-items: center;
  padding: 9px 0;
  border-top: 1px solid var(--bd-rule);
}
.bd-step-row:first-child { border-top: 0; }
.bd-check {
  width: 18px;
  height: 18px;
  border-radius: 5px;
  border: 1px solid var(--bd-rule);
  background: transparent;
  cursor: pointer;
  padding: 0;
  flex: none;
}
.bd-check.done { background: var(--bd-patina); border-color: var(--bd-patina); }
.bd-step-label {
  font-size: 13.5px;
  color: var(--bd-ink);
  min-width: 0;
  /* A pasted link is one unbroken word, so without this it refuses to wrap
     and runs out under the delete button. `anywhere` rather than
     `break-word` because only the former lets the line box shrink, which is
     what a flex child inside a narrow panel actually needs. Ordinary prose
     is unaffected: it has spaces to break at and never reaches this rule. */
  overflow-wrap: anywhere;
}
.bd-step-label.done { color: var(--bd-faint); }
.bd-kill {
  font: inherit;
  font-size: 15px;
  line-height: 1;
  color: var(--bd-faint);
  background: transparent;
  border: 0;
  cursor: pointer;
  min-width: 30px;
  min-height: 30px;
  border-radius: 6px;
}
.bd-kill:hover { color: var(--bd-clay); background: var(--bd-ground); }

.bd-err {
  background: var(--bd-ground);
  border: 1px solid var(--bd-clay);
  border-radius: 8px;
  padding: 10px 12px;
  font-size: 12.5px;
  color: var(--bd-clay);
  margin: 12px 0 0;
}

/* Unfilled and unruled. The old footer was a grey bar with a border, which
   is the single heaviest thing a small panel can end with. */
.bd-foot {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 2px 30px 22px;
  background: transparent;
  flex: none;
}
.bd-since { font-size: 11.5px; color: var(--bd-faint); }
.bd-more { font-size: 11.5px; color: var(--bd-soft); margin-left: auto; }

/* The card underneath recedes while its panel is up: it BECAME the panel,
   so leaving it at full strength would read as two of the same thing. */
.card.is-open { opacity: .18; }

body.bd-open { overflow: hidden; }

@media (max-width: 640px) {
  .bd-panel {
    left: 0;
    right: 0;
    top: auto;
    bottom: 0;
    width: 100%;
    max-height: 88vh;
    border-radius: 16px 16px 0 0;
    border-bottom: 0;
  }
  .bd-head { padding: 16px 16px 12px; }
  .bd-body { padding: 6px 16px 16px; }
  /* The footer holds the writing line and it is the last thing above the
     home indicator, so it carries the inset. Without it the Add button sits
     under the bar you swipe up on, and the swipe wins. */
  .bd-foot { padding: 12px 16px calc(12px + env(safe-area-inset-bottom)); }
  /* Only where the panel IS a bottom sheet. iOS pins fixed elements to the
     layout viewport, so at `bottom: 0` the writing line goes behind the
     keyboard the instant you focus it. */
  .bd-panel {
    bottom: var(--kb, 0px);
    /* Same rule as the dialogs above: lifted AND shortened, or the keyboard
       pushes the panel's title and its close button off the top. */
    max-height: calc(88vh - var(--kb, 0px));
  }
  /* Something to grab, and a sign that this sheet is dismissible at all. A
     bottom sheet with no handle reads as a page that arrived from below. */
  .bd-head::before {
    content: '';
    display: block;
    width: 38px; height: 4px;
    margin: -4px auto 12px;
    border-radius: 99px;
    background: var(--bd-rule);
  }
}

@media (prefers-reduced-motion: reduce) {
  .bd-scrim, .bd-panel, .bd-inner { animation: none !important; transition: none !important; }
  .bd-inner { opacity: 1; }
}

/* Touch targets in the panel. The design audit flagged the toolbar's 30px
   controls against the 44px floor; shipping a new surface with 36px controls
   would be the same mistake with fresh paint.

   The tick box keeps its 18px LOOK and grows only its hit area, through a
   transparent overlay. Inflating the visible box to 44px would distort the
   affordance to satisfy a number, which is not what the number is for. */
/* `(pointer: coarse), (max-width: 767px)`, not coarse alone. Two reasons,
   and the second is the one that bit: a browser can report `pointer: fine`
   on a small screen — this project's own embedded one does, which is why a
   QA sweep measured these controls at 18px and 30px and concluded the
   floor had been missed entirely. A rule nobody can observe is a rule
   nobody can verify. Width is the honest second signal. */
@media (pointer: coarse), (max-width: 767px) {
  .bd-x { min-width: 44px; min-height: 44px; }
  .bd-step { min-height: 44px; }
  .bd-kill { min-width: 44px; min-height: 44px; }
  .bd-check { position: relative; }
  .bd-check::after { content: ''; position: absolute; inset: -13px; }
  /* Measured at 31x31 and 149x14 on a 375px screen. Both keep their look and
     grow only what a finger has to land on, the same way the tick box does. */
  .bd-copy { position: relative; }
  .bd-copy::after { content: ''; position: absolute; inset: -7px; }
  .bd-more { padding: 15px 0; margin-top: -15px; margin-bottom: -15px; }
  .bd-view { min-height: 44px; }
  .bd-save { min-height: 44px; }
  .bd-write { min-height: 44px; }
}

/* ── page transitions ──────────────────────────────────────────────
   The remaining navigations (Focus, Review, List, Patterns) cross-fade
   instead of blinking white. One declaration, no JavaScript, and browsers
   without support simply navigate as they always did.

   Deliberately NOT applied to the canvas: a full-page cross-fade over a
   pan-and-zoom surface reads as the canvas itself moving, which is the one
   thing that must only ever happen when you move it. */
@view-transition { navigation: auto; }

::view-transition-old(root) { animation-duration: .16s; }
::view-transition-new(root) { animation-duration: .18s; }

@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(root),
  ::view-transition-new(root) { animation: none; }
}

/* ── toolbar, rebuilt ──────────────────────────────────────────────
   Icon above a small label. Nine controls used to be nine words in a row
   that scrolled off a phone; this fits them in about half the width while
   keeping the label, because an icon-only bar is a memory test and this app
   is used in bursts weeks apart. */
.tool {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-width: 54px;
  min-height: 46px;
  padding: 5px 8px 4px;
  border-radius: 9px;
  border: 1px solid transparent;
  background: transparent;
  color: var(--bd-soft);
  font: inherit;
  text-decoration: none;
  cursor: pointer;
  flex: none;
  transition: background .14s ease, color .14s ease;
}
.tool:hover { background: var(--bd-ground); color: var(--bd-ink); }
.tool[aria-expanded='true'] { background: var(--bd-ground); color: var(--bd-ink); }
.tool-l { font-size: 10px; font-weight: 500; letter-spacing: .01em; line-height: 1; }
.tool-go { color: var(--bd-brass); }
.tool-go:hover { background: var(--bd-brass); color: var(--bd-card); }

.bar-right { left: auto; right: 14px; gap: 4px; }

/* ── menus ─────────────────────────────────────────────────────────
   A dropdown rather than a page, because Share and Account were two
   navigations away from a canvas you did not want to leave. */
.menu { position: relative; }

.pop {
  position: absolute;
  top: calc(100% + 8px);
  left: 0;
  min-width: 262px;
  padding: 6px;
  background: var(--bd-card);
  border: 1px solid var(--bd-rule);
  border-radius: 12px;
  box-shadow: 0 12px 34px var(--bd-shadow), 0 2px 6px var(--bd-shadow);
  z-index: 45;
  animation: pop-in .16s cubic-bezier(.2,.9,.3,1) both;
}
.pop-end { left: auto; right: 0; }
/* Belt and braces: every menu in the right-hand cluster anchors right,
   whether or not someone remembered the modifier class. A dropdown that
   opens off the edge of the screen is invisible, not merely misaligned. */
.bar-right .pop { left: auto; right: 0; }
/* Set by menu.js when the menu would otherwise be clipped by a scrolling
   ancestor. `fixed` leaves that clipping context entirely; the coordinates it
   gives up are recomputed from the trigger. Nothing sets these by hand. */
.pop.is-loose {
  position: fixed;
  left: auto;
  right: auto;
  /* Above the bar, which is 40. */
  z-index: 60;
}

.pop[hidden] { display: none; }
@keyframes pop-in {
  from { opacity: 0; transform: translateY(-5px) scale(.985); }
  to   { opacity: 1; transform: none; }
}

.pop-i {
  display: flex;
  align-items: flex-start;
  gap: 11px;
  width: 100%;
  padding: 9px 10px;
  border-radius: 8px;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}
.pop-i:hover { background: var(--bd-ground); }
.pop-i > svg { flex: none; margin-top: 1px; color: var(--bd-faint); }
.pop-i span { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.pop-i b { font-size: 13px; font-weight: 500; color: var(--bd-ink); }
.pop-i i { font-size: 11.5px; font-style: normal; color: var(--bd-faint); line-height: 1.35; }

.pop-sep { height: 1px; background: var(--bd-rule); margin: 5px 4px; }
.pop-who { padding: 8px 10px 6px; display: flex; flex-direction: column; gap: 2px; }
.pop-who b { font-size: 13px; font-weight: 600; }
.pop-who i { font-size: 11.5px; font-style: normal; color: var(--bd-faint); overflow: hidden; text-overflow: ellipsis; }
.pop-lab {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--bd-faint);
  padding: 4px 10px 6px;
}

/* Three states, not a switch: "follow this machine" is a real answer and the
   one the app has always given, so it must stay reachable. */
.seg { display: flex; gap: 3px; padding: 0 6px 4px; }
.seg-i {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  flex: 1;
  min-height: 34px;
  padding: 0 6px;
  font: inherit;
  font-size: 11.5px;
  border-radius: 7px;
  border: 1px solid var(--bd-rule);
  background: transparent;
  color: var(--bd-soft);
  cursor: pointer;
}
.seg-i:hover { color: var(--bd-ink); }
.seg-i.on { background: var(--bd-ink); border-color: var(--bd-ink); color: var(--bd-card); }
.seg-i svg { flex: none; }

.avatar {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  border: 1px solid var(--bd-rule);
  background: var(--bd-ground);
  color: var(--bd-brass);
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
}
.avatar:hover { border-color: var(--bd-brass); }
.avatar[aria-expanded='true'] { border-color: var(--bd-brass); background: var(--bd-card); }

/* The copied-link confirmation. Not a toast that floats over the canvas:
   it belongs to the control that was pressed. */
.pop-said {
  padding: 8px 10px;
  font-size: 11.5px;
  color: var(--bd-patina);
  line-height: 1.4;
}
.pop-said.bad { color: var(--bd-clay); }

@media (pointer: coarse) {
  .tool { min-height: 48px; min-width: 58px; }
  .avatar { width: 44px; height: 44px; }
  .pop-i { padding: 12px 10px; }
}

@media (max-width: 640px) {
  .bar-right { right: 10px; }
  .pop { min-width: min(280px, calc(100vw - 24px)); }
}

@media (prefers-reduced-motion: reduce) {
  .pop { animation: none; }
}

/* ── motion ────────────────────────────────────────────────────────
   The rule: motion carries INFORMATION or it does not happen. Where a thing
   came from, what became what, whether a press registered. Decoration that
   moves is just latency you chose.

   FOCUS, chosen over a particle formation after four attempts at the latter
   read as generic however they were tuned. A card arrives slightly too large
   and out of focus and resolves sharp, like a lens pulling — which is the
   nearest thing to what this app is for: something vague in your head
   becoming a definite object you can see.

   Removal is the SAME MOTION REVERSED. Creation and deletion being one
   gesture run in two directions is most of what makes either read as
   deliberate rather than as two effects that happen to live nearby. */
.card.is-focusing { animation: card-focus .46s cubic-bezier(.2, .8, .25, 1) both; }
@keyframes card-focus {
  from { opacity: 0; filter: blur(9px); transform: scale(1.09); }
  60%  { opacity: 1; }
  to   { opacity: 1; filter: blur(0); transform: none; }
}

.card.is-going { animation: card-defocus .3s cubic-bezier(.4, 0, .7, .4) both; pointer-events: none; }
@keyframes card-defocus {
  from { opacity: 1; filter: blur(0); transform: none; }
  to   { opacity: 0; filter: blur(9px); transform: scale(1.09); }
}

.card {
  transition:
    opacity .2s ease,
    box-shadow .18s ease;
}

/* Hover is a statement that a thing is grabbable, so it lifts by exactly as
   much as a real card would. Suppressed mid-gesture: a card already in your
   hand does not need to be told it can be picked up. */
.card:hover { box-shadow: 0 4px 16px var(--bd-shadow); }
.card.is-dragging,
.card.is-resizing { transition: none; }

/* The canvas never animates its own transform. Pan and zoom must track the
   hand exactly; easing there reads as lag, not as polish. Discrete moves
   (the zoom buttons, Fit) are eased in canvas.js instead, where the app
   knows the difference between a gesture and a press. */

@media (prefers-reduced-motion: reduce) {
  .card.is-focusing, .card.is-going { animation: none !important; opacity: 1; filter: none; transform: none; }
}

/* ── the card shows what is in it ──────────────────────────────────
   A card used to be a name and one line. The steps written inside it were
   invisible until you opened it, so the card told you almost nothing.

   The list is CLIPPED, not scrolled: cards are resizable, so making one
   taller is how you see more, and the count in the meta line says what is
   hidden. A scrollbar inside a card on a pannable canvas would also fight
   the canvas for the wheel. */
.card-steps {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-height: 0;
  overflow: hidden;
  flex: 1;
}
.card-step {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 7px;
  align-items: start;
  font-size: 12.5px;
  line-height: 1.4;
  color: var(--bd-soft);
}
.card-tick {
  width: 10px;
  height: 10px;
  margin-top: 3px;
  border-radius: 3px;
  border: 1px solid var(--bd-rule);
  flex: none;
}
.card-step.done .card-tick { background: var(--accent); border-color: var(--accent); }
.card-step.done .card-step-t { color: var(--bd-faint); text-decoration: line-through; }
/* One line each on the card. The full text is a click away, and a card that
   reflows to four lines because one step is long stops being scannable. */
.card-step-t { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }

.card-count { font-variant-numeric: tabular-nums; color: var(--bd-faint); }

/* ═══ PANEL STYLE: "ruled" — dense, with ruling ═══════════════════
   Trial, 2026-08-24. Content density is not chrome weight: a notepad is a
   page with a LOT written on it and almost nothing drawn around it. The
   previous pass removed the chrome and left the content spaced like a form,
   which reads as empty rather than calm.

   Nothing here adds a border, a shadow or a container. It tightens rhythm
   and adds RULING, which is texture belonging to the paper rather than
   furniture drawn around it.
   ───────────────────────────────────────────────────────────────── */

/* Tighter everywhere. 26px between sections is a settings screen; a written
   page runs closer than that. */
:root[data-panel="ruled"] .bd-body { gap: 15px; padding: 10px 26px 16px; }
:root[data-panel="ruled"] .bd-sec { gap: 6px; }
:root[data-panel="ruled"] .bd-head { padding: 22px 26px 0; }
:root[data-panel="ruled"] .bd-foot { padding: 0 26px 18px; }
:root[data-panel="ruled"] .bd-title { font-size: 21px; }

/* Labels sit tight to what they label, the way a heading does on paper. */
:root[data-panel="ruled"] .bd-lab { font-size: 11.5px; margin: 0 0 1px; }
:root[data-panel="ruled"] .bd-hint { font-size: 11.5px; line-height: 1.45; margin-top: 1px; }

/* ── ruling ────────────────────────────────────────────────────────
   The steps and the writing area share one continuous set of lines, so the
   list and the place you add to it read as the same sheet rather than a
   list with a form under it. Faint: it should register as paper, not as a
   table. */
:root[data-panel="ruled"] .bd-steps { margin-top: 2px; }
:root[data-panel="ruled"] .bd-step-row {
  min-height: 30px;
  padding: 5px 0 4px;
  border-bottom: 1px solid color-mix(in srgb, var(--bd-rule) 55%, transparent);
  align-items: flex-start;
}
:root[data-panel="ruled"] .bd-step-row:first-child {
  border-top: 1px solid color-mix(in srgb, var(--bd-rule) 55%, transparent);
}
:root[data-panel="ruled"] .bd-step-label { font-size: 14px; line-height: 1.45; }
:root[data-panel="ruled"] .bd-check { width: 16px; height: 16px; margin-top: 2px; }

/* The writing line continues the ruling rather than starting a new element. */
:root[data-panel="ruled"] .bd-row-write { align-items: flex-start; gap: 4px; }
:root[data-panel="ruled"] .bd-write {
  font-size: 14px;
  line-height: 1.45;
  padding: 6px 0 5px;
  min-height: 30px;
  border-bottom-color: color-mix(in srgb, var(--bd-rule) 55%, transparent);
}
:root[data-panel="ruled"] .bd-write:focus { border-bottom-color: var(--bd-brass); }
:root[data-panel="ruled"] .bd-save { min-height: 30px; padding: 0 6px; font-size: 12.5px; }

/* The next-step line is the one field that should still feel like a field,
   because it is the single most important thing on the page. */
:root[data-panel="ruled"] .bd-input:not(.bd-write) { font-size: 14.5px; min-height: 34px; padding: 5px 2px; }

/* Stage buttons closer together and shorter: a row of choices, not a toolbar. */
:root[data-panel="ruled"] .bd-pipe { gap: 4px; }
:root[data-panel="ruled"] .bd-step { min-height: 32px; padding: 6px 11px; font-size: 12px; }

/* ═══ PANEL STYLE: "framed" — the original ═════════════════════════
   Borders, a double shadow, section rules, a filled footer and micro-caps
   labels. It is the heaviest of the three and it is deliberately preserved:
   the density argument is a matter of taste, and taste belongs in a setting
   rather than in whichever direction the last conversation went.
   ───────────────────────────────────────────────────────────────── */
:root[data-panel="framed"] .bd-scrim {
  background: color-mix(in srgb, var(--bd-ground) 55%, transparent);
  backdrop-filter: blur(7px) saturate(120%);
  -webkit-backdrop-filter: blur(7px) saturate(120%);
}
:root[data-panel="framed"] .bd-panel {
  width: min(760px, 92vw);
  border-radius: 16px;
  box-shadow: 0 24px 64px var(--bd-shadow), 0 3px 12px var(--bd-shadow);
}
:root[data-panel="framed"] .bd-head {
  padding: 20px 22px 14px;
  border-bottom: 1px solid var(--bd-rule);
}
:root[data-panel="framed"] .bd-title { font-size: 20px; }
:root[data-panel="framed"] .bd-body { padding: 6px 22px 18px; gap: 22px; }
:root[data-panel="framed"] .bd-sec { gap: 9px; }
:root[data-panel="framed"] .bd-lab {
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: .07em;
  text-transform: uppercase;
  margin: 12px 0 0;
}
:root[data-panel="framed"] .bd-foot {
  padding: 12px 22px;
  border-top: 1px solid var(--bd-rule);
  background: var(--bd-ground);
}
:root[data-panel="framed"] .bd-input {
  background: var(--bd-ground);
  border: 1px solid var(--bd-rule);
  border-radius: 8px;
  padding: 10px 12px;
  min-height: 42px;
  font-size: 14px;
}
:root[data-panel="framed"] .bd-input:focus {
  outline: 2px solid var(--bd-brass);
  outline-offset: 1px;
  border-color: transparent;
  padding: 10px 12px;
}
:root[data-panel="framed"] .bd-write { padding: 10px 12px; }
:root[data-panel="framed"] .bd-save {
  background: var(--bd-brass);
  color: var(--bd-card);
  border: 1px solid transparent;
  border-radius: 8px;
  padding: 0 16px;
  min-height: 42px;
  font-weight: 500;
  align-self: auto;
}
:root[data-panel="framed"] .bd-save:hover { color: var(--bd-card); }
:root[data-panel="framed"] .bd-step {
  border: 1px solid var(--bd-rule);
  background: transparent;
  min-height: 40px;
  padding: 9px 14px;
  font-size: 12.5px;
}
:root[data-panel="framed"] .bd-step:hover { border-color: var(--bd-faint); background: transparent; }
:root[data-panel="framed"] .bd-step.on { background: var(--bd-ink); color: var(--bd-card); }
:root[data-panel="framed"] .bd-step-row {
  border-top: 1px solid var(--bd-rule);
  border-bottom: 0;
  padding: 9px 0;
  align-items: center;
}
:root[data-panel="framed"] .bd-step-row:first-child { border-top: 0; }
:root[data-panel="framed"] .bd-step-label { font-size: 13.5px; }
:root[data-panel="framed"] .bd-kill { opacity: 1; }

/* The segmented controls on /settings sit in a document column rather than a
   260px menu, so they get room to breathe instead of inheriting menu metrics. */
.seg-wide { padding: 0; gap: 6px; max-width: 26rem; }
.seg-wide .seg-i { min-height: 40px; font-size: 12.5px; }

/* ── alignment guides ──────────────────────────────────────────────
   Shown only while a card is actually caught on something. They live in
   `.world`, so they are in canvas coordinates and line up with the cards
   they refer to at any pan or zoom.

   Deliberately thin and brass rather than a heavy line: it is a confirmation
   that the snap happened, not a piece of furniture. */
.guide {
  position: absolute;
  display: none;
  background: var(--bd-brass);
  opacity: .55;
  pointer-events: none;
  z-index: 5;
}
.guide-x { top: -100000px; height: 200000px; width: 1px; }
.guide-y { left: -100000px; width: 200000px; height: 1px; }

/* ═══ PANEL: instant, drawn, and pinned ════════════════════════════ */

/* ── 17: a drawn tick, and no green ────────────────────────────────
   The filled green square was a semantic "success" colour in an app that
   deliberately has none, and a block of colour says "this cell is on" where
   a tick says "this is finished" — which is the thing being recorded. */
.bd-check {
  display: grid;
  place-items: center;
  padding: 0;
  background: transparent;
  border: 1.5px solid var(--bd-rule);
  transition: border-color .12s ease, background .12s ease;
}
.bd-check svg {
  width: 78%;
  height: 78%;
  fill: none;
  stroke: var(--bd-card);
  stroke-width: 3;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 24;
  stroke-dashoffset: 24;
  opacity: 0;
}
.bd-check.done { background: var(--bd-ink); border-color: var(--bd-ink); }
/* The tick DRAWS itself rather than appearing. 160ms, which is long enough
   to read as a stroke and short enough never to be waited for. */
.bd-check.done svg {
  opacity: 1;
  stroke-dashoffset: 0;
  transition: stroke-dashoffset .16s ease-out, opacity .06s linear;
}
.bd-check:hover { border-color: var(--bd-faint); }

/* ── 19: the strike CUTS across, left to right ─────────────────────
   text-decoration cannot be animated, so the line is drawn as a scaling
   pseudo-element instead. It is the one place in this app where animation
   carries meaning rather than decorating: you watch the thing be crossed off. */
/* ── the cut, drawn by the text itself ─────────────────────────────
   This was an absolutely positioned bar at left:0 right:0, wiped in with
   scaleX. It was wrong in two ways at once, and both are visible on this
   owner's own board because his steps are long:

     · THE LABEL IS A 1fr GRID COLUMN, so the bar spanned the whole column
       rather than the words. On a short step in a 700px panel the line ran
       hundreds of pixels past the text. Reported as "it doesnt end where the
       line actually ends".
     · THE LABEL WRAPS. A single bar at one offset strikes the FIRST line
       only and leaves the rest of a three-line step untouched.

   Nothing positioned can fix the second one: only `text-decoration` follows
   the glyphs, across every line, ending exactly at the last word.

   So the line is real text-decoration now and the ANIMATION moved to its
   colour. It fades in over 260ms instead of wiping left to right. A wipe
   that is geometrically wrong is worse than a fade that is right, and there
   is no way to wipe wrapped text without drawing it per line fragment. */
.bd-step-label {
  transition: color .22s ease .06s, text-decoration-color .26s cubic-bezier(.4, 0, .2, 1);
  text-decoration: line-through;
  text-decoration-thickness: 1.5px;
  /* Present but invisible at rest, so the transition has something to move
     between. Declaring the decoration only in the .done state would give the
     browser nothing to interpolate from and it would simply appear. */
  text-decoration-color: transparent;
}
.bd-step-label.done { color: var(--bd-faint); text-decoration-color: currentColor; }

/* A row that has not been confirmed by the server yet. Barely visible on
   purpose: it is there so nothing shifts when the real row replaces it. */
.bd-step-row.is-pending { opacity: .72; }


/* ── a link, drawn as a link ───────────────────────────────────────
   The title where a connector knows one, the tidied address where it does
   not, and the host underneath so you can see where it goes before you press
   it. Underlined only on hover: a card holding six links should not read as
   six underlined lines of blue.

   `inline-flex` with the host on its own line, rather than a pill, because a
   pill next to the connector's own pill is two badges arguing on one row. */
.bd-chip {
  display: inline-flex;
  flex-direction: column;
  gap: 1px;
  color: inherit;
  text-decoration: none;
  border-radius: 5px;
  padding: 1px 4px;
  margin: -1px -4px;
  max-width: 100%;
  vertical-align: top;
}
.bd-chip-t {
  overflow-wrap: anywhere;
  text-decoration: underline;
  text-decoration-color: transparent;
  text-underline-offset: 2px;
  transition: text-decoration-color .14s ease;
}
.bd-chip-h {
  font-size: 10.5px;
  color: var(--bd-faint);
  letter-spacing: .02em;
}
.bd-chip:hover .bd-chip-t { text-decoration-color: var(--bd-brass); }
.bd-chip:focus-visible { outline: 2px solid var(--bd-brass); outline-offset: 1px; }
/* A finished step is struck through by `.done` on the label; the chip has to
   inherit that rather than opting out of it. */
.bd-step-label.done .bd-chip-t { text-decoration-line: line-through underline; }

/* ── what the world says about a step ──────────────────────────────
   The first thing on this canvas whose status did not come from the person
   who typed it. Deliberately small and set into the line rather than given a
   column of its own: it is an annotation on what you wrote, not a second
   piece of information competing with it.

   NO RED on 'taken'. A domain somebody else owns is a fact, not a failure,
   and the rule against alarming colour holds for a connector exactly as it
   holds for a cold project. */
.bd-dom {
  display: inline-block;
  font-size: 9.5px;
  letter-spacing: .05em;
  text-transform: uppercase;
  padding: 1px 6px;
  border-radius: 999px;
  border: 1px solid currentColor;
  vertical-align: 1px;
  white-space: nowrap;
  /* The strike-through on a finished step belongs to the words, not to an
     annotation about them. */
  text-decoration: none;
}
.bd-dom.free { color: var(--bd-patina); }
.bd-dom.taken { color: var(--bd-faint); }

/* ── a step you decided against ────────────────────────────────────
   It sits in the folded history beside the finished ones and is deliberately
   quieter than they are: a finished step is something you did, this is
   something you chose not to. Both are worth keeping and only one is an
   achievement.

   No tick box. Ticking it would claim it happened. */
.bd-step-row.is-dropped { grid-template-columns: auto 1fr auto; opacity: .62; }
.bd-drop-mark {
  width: 18px;
  text-align: center;
  color: var(--bd-faint);
  font-size: 13px;
  line-height: 1;
}
.bd-drop-note {
  font-size: 10.5px;
  color: var(--bd-faint);
  white-space: nowrap;
}

/* ── the folded history ────────────────────────────────────────────
   Finished steps are kept and folded, never dropped: "I want to see the
   history of what was done, its a good feeling". This is the line that
   stands in for them. Deliberately quiet — it is a count, not a control
   anyone needs to notice on a short card. */
.bd-fold {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  border: 0;
  background: none;
  cursor: pointer;
  font: inherit;
  font-size: 11.5px;
  color: var(--bd-faint);
  padding: 5px 0 7px;
  text-align: left;
}
.bd-fold::after {
  content: '';
  flex: 1 1 auto;
  height: 1px;
  background: var(--bd-rule);
}
.bd-fold-a { color: var(--bd-brass); order: 3; flex: none; }
.bd-fold:hover .bd-fold-a { text-decoration: underline; }
.bd-fold-n { flex: none; font-variant-numeric: tabular-nums; }

/* ── 16 + 26: the writing line is pinned ───────────────────────────
   With sixty steps written, adding another meant scrolling to the bottom of
   a long list, and the box then scrolled away again while you typed. Sticky
   keeps it in flow — you still write at the END of the list, which is the
   right mental model — while never letting it leave the screen.

   Applies to all three panel styles, so it is set on the base rule. */
.bd-row-write {
  position: sticky;
  bottom: 0;
  z-index: 2;
  background: var(--bd-card);
  padding-top: 10px;
  margin-top: 2px;
}
/* A short fade above it, so a step scrolling underneath does not collide
   with the writing line. */
.bd-row-write::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: -14px;
  height: 14px;
  background: linear-gradient(to top, var(--bd-card), transparent);
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .bd-check svg, .bd-step-label { transition: none !important; }
}

/* ═══ 4: the toolbar answers your hand ═════════════════════════════
   Icons lift and the label follows. Small enough to register as response
   rather than as an effect: 1px and 4% is felt, not watched. */
.tool svg, .avatar { transition: transform .16s cubic-bezier(.2,.9,.3,1), color .14s ease; }
.tool:hover svg { transform: translateY(-1px) scale(1.06); }
.tool:active svg { transform: translateY(0) scale(.96); transition-duration: .08s; }
.avatar:hover { transform: scale(1.06); }
.avatar:active { transform: scale(.96); transition-duration: .08s; }

/* ═══ 14/15: the bin ═══════════════════════════════════════════════
   Absent until a card is in your hand. It rises from below rather than
   fading in, so it reads as arriving for this gesture rather than having
   been there unnoticed. */
.bin {
  position: fixed;
  left: 50%;
  bottom: 26px;
  z-index: 42;
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 12px 18px;
  border-radius: 12px;
  background: var(--bd-chrome);
  backdrop-filter: blur(14px) saturate(1.5);
  border: 1px solid var(--bd-rule);
  box-shadow: 0 6px 22px var(--bd-shadow);
  color: var(--bd-soft);
  font-size: 12.5px;
  pointer-events: none;
  opacity: 0;
  transform: translate(-50%, 18px) scale(.96);
  transition: opacity .18s ease, transform .22s cubic-bezier(.2,.9,.3,1), border-color .14s ease, color .14s ease;
}
.bin.is-up { opacity: 1; transform: translate(-50%, 0) scale(1); }
/* Hot: you are over it and letting go will remove the card. It has to be
   unmistakable, because the next thing that happens is a deletion. */
.bin.is-hot {
  color: var(--bd-clay);
  border-color: var(--bd-clay);
  transform: translate(-50%, 0) scale(1.06);
}
.bin svg { width: 20px; height: 20px; flex: none; overflow: visible; }

/* ── the bin opens ─────────────────────────────────────────────────
   A bin that only changes colour when you hover it is a coloured icon. A
   bin that OPENS is telling you where the card is about to go, which is the
   one thing worth saying at that moment.

   Hinged at the left end of the lid, so it swings up the way a pedal bin
   does rather than floating off. `overflow: visible` on the svg matters:
   the lid leaves the 24-box when it rotates and would otherwise be clipped
   in half mid-animation. */
.bin-lid {
  transform-box: fill-box;
  transform-origin: 8% 90%;
  transition: transform .24s cubic-bezier(.2, .9, .3, 1);
}
.bin.is-hot .bin-lid { transform: translate(-1px, -2px) rotate(-18deg); }

/* The moment of the drop. The can dips as it takes the weight and the lines
   inside compress — a reaction, not a state, so it plays once and is gone. */
.bin.is-eating { animation: bin-eat .42s cubic-bezier(.3, .8, .3, 1); }
.bin.is-eating .bin-lid { transform: none; }
.bin.is-eating .bin-lines { animation: bin-lines .42s ease; }
@keyframes bin-eat {
  0%   { transform: translate(-50%, 0) scale(1.06); }
  35%  { transform: translate(-50%, 4px) scale(.94); }
  100% { transform: translate(-50%, 0) scale(1); }
}
@keyframes bin-lines {
  0%   { transform: scaleY(1); }
  35%  { transform: scaleY(.7); }
  100% { transform: scaleY(1); }
}
.bin-lines { transform-box: fill-box; transform-origin: center bottom; }

/* `.card.is-going` is defined with the motion rules, as the reverse of the
   arrival. Do not restate it here: a plain opacity rule further down the
   sheet silently beat the keyframe and the card simply vanished. */

/* ═══ the toast, which is really the undo ══════════════════════════
   Nine seconds, not three. A short undo on a delete is a delete with a
   decorative apology attached. */
.toast {
  position: fixed;
  left: 50%;
  bottom: 26px;
  z-index: 43;
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 12px 16px;
  border-radius: 12px;
  background: var(--bd-ink);
  color: var(--bd-card);
  font-size: 13px;
  box-shadow: 0 8px 26px var(--bd-shadow);
  opacity: 0;
  transform: translate(-50%, 18px);
  transition: opacity .2s ease, transform .24s cubic-bezier(.2,.9,.3,1);
  pointer-events: none;
}
.toast.is-up { opacity: 1; transform: translate(-50%, 0); pointer-events: auto; }
.toast-undo {
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  color: var(--bd-brass);
  background: transparent;
  border: 0;
  cursor: pointer;
  padding: 4px 2px;
  min-height: 32px;
}
.toast-undo:hover { color: var(--bd-card); }

/* ═══ 8: copy, revealed rather than presented ══════════════════════ */
.bd-sec-head { display: flex; align-items: center; gap: 8px; }
.bd-copy {
  font: inherit;
  background: transparent;
  border: 0;
  color: var(--bd-faint);
  cursor: pointer;
  padding: 4px;
  border-radius: 6px;
  display: grid;
  place-items: center;
  opacity: 0;
  transition: opacity .14s ease, color .14s ease;
  flex: none;
}
.bd-copy svg { width: 15px; height: 15px; }
.bd-step-row:hover .bd-copy,
.bd-sec-head:hover .bd-copy,
.bd-copy:focus-visible { opacity: 1; }
.bd-copy:hover { color: var(--bd-ink); }
/* Confirmation is the icon turning, not a message appearing. */
.bd-copy.did { opacity: 1; color: var(--bd-patina); }
.bd-copy.failed { opacity: 1; color: var(--bd-clay); }
.bd-copy-all { margin-left: auto; }

.bd-step-row { grid-template-columns: auto 1fr auto auto; }

@media (pointer: coarse) {
  /* No hover on touch, so they stay visible rather than unreachable. */
  .bd-copy { opacity: .55; padding: 8px; }
}
@media (prefers-reduced-motion: reduce) {
  .bin, .toast, .tool svg, .avatar, .bd-copy { transition: none !important; }
  .bin-lid, .bin-lines { transition: none !important; animation: none !important; transform: none !important; }
  .bin.is-eating { animation: none !important; }
}

/* ═══ 32: CANVAS PALETTES ══════════════════════════════════════════
   The ground and the accent, which is what "the canvas bg color and accent"
   meant. The ink ramp and the card do NOT change: those were solved once for
   readability and are not a matter of taste.

   `brass` is the palette you have and writes no attribute, so the base
   stylesheet remains the default rather than a fourth state.

   Every accent below was solved to clear 4.5:1 against BOTH its own ground
   and the card, because the accent is text — project names, stage labels,
   the primary button. Picking these by eye is how the last palette shipped
   failing at every size it was used.
   ───────────────────────────────────────────────────────────────── */

/* ── slate: cool, quiet, technical ── */
:root[data-canvas="slate"] {
  --bd-ground:   #E1E5EA;
  --bd-grid:     rgba(22, 28, 38, .06);
  --bd-grid-big: rgba(22, 28, 38, .095);
  --bd-rule:     #C6CCD5;
  --bd-brass:    #38699E;
}
@media (prefers-color-scheme: dark) {
  :root[data-canvas="slate"]:not([data-theme="light"]) {
    /* Was #15181D — one channel-step from the brass ground, so at night Slate
       and Brass were the same canvas. Properly blue now. */
    --bd-ground:   #141A26;
    --bd-grid:     rgba(220, 230, 244, .05);
    --bd-grid-big: rgba(220, 230, 244, .08);
    --bd-rule:     #313945;
    --bd-brass:    #7FB0E0;
  }
}
:root[data-canvas="slate"][data-theme="dark"] {
  /* Was #15181D — one channel-step from the brass ground, so at night Slate
     and Brass were the same canvas. Properly blue now. */
  --bd-ground:   #141A26;
  --bd-grid:     rgba(220, 230, 244, .05);
  --bd-grid-big: rgba(220, 230, 244, .08);
  --bd-rule:     #313945;
  --bd-brass:    #7FB0E0;
}

/* ── moss: soft, green-grey, closest to paper on a desk ── */
:root[data-canvas="moss"] {
  --bd-ground:   #E3E8DE;
  --bd-grid:     rgba(24, 34, 22, .06);
  --bd-grid-big: rgba(24, 34, 22, .095);
  --bd-rule:     #C8D0C2;
  --bd-brass:    #457238;
}
@media (prefers-color-scheme: dark) {
  :root[data-canvas="moss"]:not([data-theme="light"]) {
    --bd-ground:   #141D15;
    --bd-grid:     rgba(226, 238, 222, .05);
    --bd-grid-big: rgba(226, 238, 222, .08);
    --bd-rule:     #2F3730;
    --bd-brass:    #8FBF7A;
  }
}
:root[data-canvas="moss"][data-theme="dark"] {
  --bd-ground:   #141D15;
  --bd-grid:     rgba(226, 238, 222, .05);
  --bd-grid-big: rgba(226, 238, 222, .08);
  --bd-rule:     #2F3730;
  --bd-brass:    #8FBF7A;
}

/* A swatch that shows the actual palette rather than naming it. */
.pal { display: flex; gap: 8px; flex-wrap: wrap; padding: 0; max-width: 26rem; }
.pal-i {
  font: inherit;
  font-size: 12px;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px 8px 9px;
  min-height: 42px;
  border-radius: 9px;
  border: 1px solid var(--bd-rule);
  background: transparent;
  color: var(--bd-soft);
  cursor: pointer;
}
.pal-i:hover { color: var(--bd-ink); }
.pal-i.on { border-color: var(--bd-ink); color: var(--bd-ink); }
.pal-dot {
  width: 22px; height: 22px; border-radius: 6px; flex: none;
  border: 1px solid rgba(27, 26, 22, .12);
  display: grid; place-items: center;
}
.pal-dot i { width: 9px; height: 9px; border-radius: 2px; display: block; }

/* ═══ 21: the toolbar opens panels, not pages ══════════════════════
   Wider and taller than the project panel, because these are documents
   rather than one card's fields, and they carry their own headings. */
.sheet-panel {
  width: min(880px, 94vw);
  max-height: min(760px, 90vh);
}
.sheet-panel .bd-inner { position: relative; min-height: 0; }
.sheet-body-scroll {
  overflow-y: auto;
  min-height: 0;
  padding: 4px 0 8px;
}
/* The page inside was written for a document column and already carries its
   own measure, so it only needs its outer padding neutralised. */
.sheet-body-scroll .page {
  max-width: none;
  margin: 0;
  padding: 26px 30px 22px;
}
.sheet-body-scroll .topbar { display: none; }
.sheet-panel .bd-x {
  position: absolute;
  top: 14px;
  right: 16px;
  z-index: 2;
}

@media (max-width: 640px) {
  .sheet-body-scroll .page { padding: 20px 16px 16px; }
}

/* Cards glide to their new places rather than teleporting, so you can see
   WHERE each one went. Only while tidying: a transition on left/top during a
   drag would make every card lag behind the cursor. */
.card.is-tidying { transition: left .38s cubic-bezier(.2,.9,.25,1), top .38s cubic-bezier(.2,.9,.25,1); }
@media (prefers-reduced-motion: reduce) { .card.is-tidying { transition: none; } }

/* ═══ 31: views ════════════════════════════════════════════════════
   A view is a lens, not a folder: a project can be in several, because a
   piece of client work genuinely belongs in both "Arhum" and "This week",
   and forcing one home makes people pick and lose the other. */
.tool-view .tool-l {
  max-width: 84px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Filtering is visible at a glance, so you cannot mistake a view for an
   empty canvas or wonder where your cards went. */
body.in-view .tool-view { color: var(--bd-brass); }
body.in-view .tool-view svg { color: var(--bd-brass); }

.view-list { display: flex; flex-direction: column; max-height: 46vh; overflow-y: auto; }
.view-i { position: relative; padding-right: 34px; }
.view-i.on b { color: var(--bd-brass); }
.view-x {
  position: absolute;
  right: 8px;
  top: 50%;
  transform: translateY(-50%);
  width: 26px;
  height: 26px;
  display: grid;
  place-items: center;
  border-radius: 6px;
  color: var(--bd-faint);
  font-size: 15px;
  opacity: 0;
  transition: opacity .14s ease, color .14s ease;
}
.view-i:hover .view-x, .view-x:focus-visible { opacity: 1; }
.view-x:hover { color: var(--bd-clay); background: var(--bd-ground); }

.view-new { display: flex; gap: 6px; align-items: center; padding: 2px 8px 4px; }
.view-new .bd-input { font-size: 13px; min-height: 34px; }

/* Membership inside the project panel. Chips, not a checklist: you are
   tagging, and a column of checkboxes makes three views look like a form. */
.bd-views { display: flex; flex-wrap: wrap; gap: 6px; }
.bd-view {
  font: inherit;
  font-size: 12px;
  padding: 6px 11px;
  min-height: 32px;
  border-radius: 999px;
  border: 1px solid var(--bd-rule);
  background: transparent;
  color: var(--bd-soft);
  cursor: pointer;
  transition: background .14s ease, color .14s ease, border-color .14s ease;
}
.bd-view:hover { color: var(--bd-ink); border-color: var(--bd-faint); }
.bd-view.on { background: var(--bd-ink); border-color: var(--bd-ink); color: var(--bd-card); }

/* Delete-a-view is a small DESTRUCTIVE target sitting inside the large benign
   one that switches views. It keeps its quiet look and gets a real hit area,
   the same treatment the tick box gets. */
@media (pointer: coarse), (max-width: 767px) {
  .view-x { opacity: .6; position: relative; }
  .view-x::after { content: ''; position: absolute; inset: -9px; }
}

/* ═══ guests on a shared board ═════════════════════════════════════
   Identity without an account: a name and a colour, and everything they
   touch carries it. The name is a CLAIM and is never shown as verified —
   what is true is that they came through this link. */
.who { display: inline-flex; align-items: center; gap: 7px; font-size: 12.5px; color: var(--bd-soft); padding-right: 4px; }
.who-dot { width: 10px; height: 10px; border-radius: 50%; flex: none; }

/* The role and colour pickers reuse the card swatches, so a guest's colour is
   already part of the palette rather than an arbitrary hex out of a form. */
.seg-i input[type="radio"] { position: absolute; opacity: 0; pointer-events: none; }
.seg-i:has(input:checked) { background: var(--bd-ink); border-color: var(--bd-ink); color: var(--bd-card); }

/* A step ticked by a guest carries their colour, so the board tells you who
   did what without a name against every line. */
.bd-check.done.g-brass  { background: var(--bd-brass);  border-color: var(--bd-brass); }
.bd-check.done.g-patina { background: var(--bd-patina); border-color: var(--bd-patina); }
.bd-check.done.g-clay   { background: var(--bd-clay);   border-color: var(--bd-clay); }
.bd-check.done.g-slate  { background: var(--bd-slate);  border-color: var(--bd-slate); }
.bd-check.done.g-plum   { background: var(--bd-plum);   border-color: var(--bd-plum); }
.bd-check.done.g-moss   { background: var(--bd-moss);   border-color: var(--bd-moss); }

/* ══════════════════════════════════════════════════════════════════════
   THE PHONE
   ══════════════════════════════════════════════════════════════════════

   Measured on a 375x812 screen before a line of this was written, because
   the failure was not subtle and it was not going to be found by looking:

     · the two floating toolbars OVERLAPPED BY 190px. Review, List, Patterns
       and Ask all sat under the right-hand cluster and could not be pressed.
       Ask — the assistant — was completely unreachable on a phone.
     · Fit landed at 20% zoom, where a 280px card is 56px wide. Eight cards
       were unreadable specks. The owner's real board has twenty.
     · the zoom controls were 28x27 against a 44px floor this project's own
       design audit set.
     · there was no `viewport-fit=cover`, so every env(safe-area-inset-*)
       resolved to 0 and the bars would sit under the Dynamic Island and the
       home indicator on a real iPhone while every rule read back correct.

   Two decisions follow, and the second is the one that matters.

   NAVIGATION MOVES TO THE BOTTOM. A thumb rests at the bottom of a phone;
   the top of an 812px screen is the one place it cannot reach without
   regripping the device. Five slots, the middle one raised.

   THE CANVAS STOPS BEING THE HOME. This is the real change and it is worth
   being honest about why. An infinite canvas is a DESKTOP metaphor: it works
   because a large screen can hold a neighbourhood of cards at a readable
   size, and spatial memory does the finding. A 375px screen can hold about
   one card at a readable size. Fitting the board means 20%, and at 20% there
   is nothing to remember the position OF. The spatial idea does not shrink.

   So on a phone the default is the DECK: the same cards, the same renderer,
   the same handlers, laid out as a vertical list you can actually read. The
   board is one tap away for anyone who wants it. This is the views lesson
   applied to layout — a DOM state, not a route, not a second renderer, and
   not a second set of data.

   The deck is also the DEFAULT IN CSS RATHER THAN IN JAVASCRIPT, so a phone
   gets the readable layout on the first paint with no flash of a canvas it
   was never going to be able to use, and with no inline script to add to a
   hash-based CSP that deliberately allows exactly one. */

/* ── the tab bar ──────────────────────────────────────────────────────
   Hidden by default and shown only on a phone: on a desktop the toolbar is
   correct and a second navigation would be two answers to one question. */
.tabs { display: none; }

/* The breakpoint is width OR a short screen on a small device, not width
   alone. A phone on its side is 812x375: wider than 767, so the whole mobile
   shell switched off and handed a touch device the desktop toolbar with 29px
   buttons and 30px swatches. Rotating a phone is not an exotic case.

   The second condition is bounded by width as well so a short desktop window
   never inherits a phone's navigation — 950px covers every iPhone in
   landscape (the largest is 932) and no ordinary desktop window is both
   under 950 wide and under 500 tall. Deliberately NOT keyed on
   `pointer: coarse`, which this project has just been reminded is reported
   as `fine` by browsers on small screens. */
@media (max-width: 767px), (max-height: 500px) and (max-width: 950px) {

  /* ── 1. the top of the screen ──────────────────────────────────────
     The left bar keeps only the wordmark, so the two bars stop fighting for
     the same 375px. Everything that was in it is in the tab bar or behind
     More. The right cluster keeps views, share and the account, which are
     the three things that answer "where am I and who am I", and at three
     controls it fits with room to spare. */
  /* The OWNER's left bar goes entirely. Its wordmark was already hidden at
     640px and everything else in it is now in the tab bar or behind More, so
     what was left was a 355x2px empty pill floating over the deck — measured,
     not guessed. An element with nothing in it is not harmless: it still
     spans the width and still sits above the content.

     `:not(.shared)` is load-bearing. On a share link that same bar carries
     the board's LABEL and the guest's own name, which is the only thing on
     the page saying whose board this is and who the app thinks you are.
     Hiding it there would be deleting the answer to both questions. */
  body:not(.shared) .bar:not(.bar-right) { display: none; }
  body.shared .bar:not(.bar-right) {
    max-width: calc(100vw - 96px - env(safe-area-inset-left) - env(safe-area-inset-right));
    left: calc(10px + env(safe-area-inset-left));
    top: calc(10px + env(safe-area-inset-top));
    padding: 0 12px;
  }
  body.shared .bar .bar-name {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  /* Two slots, not five, and they should not be stretched half a screen wide
     apiece — a tab that is 187px of empty space with a word in the middle
     reads as a broken layout. */
  /* `.tabs.tabs-pair`, not `.tabs-pair`. Measured: the columns came back as
     "71px 71px 71px 71px 71px" — five, not two — because both selectors are
     one class and `.tabs` is written further down the block, so it won on
     order. justify-content applied and did nothing visible, which is exactly
     the kind of half-applied rule that reads as "centring is broken". */
  .tabs.tabs-pair { grid-template-columns: repeat(2, minmax(0, 132px)); justify-content: center; }
  .bar-right {
    top: calc(10px + env(safe-area-inset-top));
    right: calc(10px + env(safe-area-inset-right));
  }

  /* ── 2. the tab bar ────────────────────────────────────────────────
     `padding-bottom` carries the home indicator rather than a magic number,
     and `100%` of nothing on a phone without one, so the same rule is right
     on every device. */
  .tabs {
    position: fixed;
    left: 0; right: 0; bottom: 0;
    z-index: 60;
    display: grid;
    grid-template-columns: repeat(5, 1fr);
    align-items: end;
    gap: 2px;
    padding: 6px 6px calc(6px + env(safe-area-inset-bottom));
    background: color-mix(in srgb, var(--bd-card) 88%, transparent);
    backdrop-filter: blur(14px) saturate(1.1);
    -webkit-backdrop-filter: blur(14px) saturate(1.1);
    border-top: 1px solid var(--bd-rule);
  }
  .tab {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 3px;
    /* 52px, comfortably past the 44px floor, and the whole column is the
       target rather than the glyph inside it. */
    min-height: 52px;
    padding: 4px 2px;
    border: 0;
    border-radius: 12px;
    background: none;
    font: inherit;
    color: var(--bd-faint);
    text-decoration: none;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: color .16s ease, background-color .16s ease;
  }
  .tab svg { width: 22px; height: 22px; }
  .tab-l { font-size: 10.5px; letter-spacing: .01em; }
  /* Colour ALONE is not a state. The selected tab also carries a short brass
     rule above it, so it is still readable to anyone who cannot separate the
     brass from the faint — which on a small dim screen outdoors is most
     people, not an edge case. */
  .tab[aria-pressed="true"] { color: var(--bd-brass); }
  .tab[aria-pressed="true"]::before {
    content: '';
    position: absolute;
    top: -6px;
    width: 20px;
    height: 2.5px;
    border-radius: 99px;
    background: var(--bd-brass);
  }
  .tab { position: relative; }
  .tab:active { background: var(--bd-sunk); }
  .tab:focus-visible { outline: 2px solid var(--bd-brass); outline-offset: -2px; }

  /* The one control here that MAKES something rather than shows something.
     Round, raised and unlabelled so it does not read as the third of five
     equal tabs, and sitting on the most reachable point of the screen. */
  .tab-new {
    align-self: center;
    width: 56px; height: 56px;
    min-height: 56px;
    margin-top: -16px;
    border-radius: 50%;
    background: linear-gradient(178deg, var(--bd-brass-lit, #E8C77A), var(--bd-brass));
    color: #241A08;
    box-shadow: 0 6px 16px -4px rgba(0,0,0,.42), 0 1px 0 rgba(255,255,255,.35) inset;
  }
  .tab-new svg { width: 26px; height: 26px; }
  .tab-new:active { background: var(--bd-brass); transform: scale(.94); }

  /* ── the bar has to GET OUT OF THE WAY ─────────────────────────────
     The tab bar shipped at z-index 60. Every bottom-anchored surface this
     app already had is 40 to 45 — scrim 40, panel 41, bin 42, toast 43 —
     and on a phone all of them are `bottom: 0`. So the new bar painted on
     top of all of them:

       · the project panel's whole footer, which is the writing line and Add,
         sat underneath it. That is the first thing anybody does after
         opening a card.
       · so did the footer of every panelled page behind More.
       · the BIN was invisible while still live, because `overBin()` tests a
         rect and a rect knows nothing about paint order — you could drop a
         card onto the bar and delete it having never seen a bin.
       · and the "Put them back" toast was invisible too, so the undo for
         that deletion could not be pressed either.

     Lowering the bar under the scrim is the wrong fix: it would then sit
     under the deck's own cards. The bar is not a layer, it is a MODE — it
     belongs to the deck, and it should leave whenever something modal is up
     or a card is in the air. `bd-open` is already set by both panel.js and
     sheet.js, and `is-grabbing` for the length of a drag, so both states are
     already published and neither needs new bookkeeping.

     Fourth instance of the ancestor-and-paint-order family in this file,
     after overflow clipping a menu, backdrop-filter capturing a fixed
     descendant, and the top layer beating the custom cursor. Every rule read
     back correct in all four. */
  body.bd-open .tabs,
  body.is-grabbing .tabs {
    transform: translateY(110%);
    pointer-events: none;
  }
  .tabs { transition: transform .22s cubic-bezier(.2, .8, .25, 1); }

  /* The bin and the toast still have to beat the bar on the frame before it
     has finished leaving, and the toast has to sit ABOVE where the bar was
     or it appears behind it the moment the bar slides back. */
  .bin { z-index: 61; }
  .toast {
    z-index: 61;
    bottom: calc(26px + 68px + env(safe-area-inset-bottom));
  }

  /* ── 3. the deck, which is the default ─────────────────────────────
     The same cards. `!important` is the correct tool and not a shortcut:
     canvas.js writes `transform` to `.world` as an INLINE style on every
     frame, and the card's left/top/width are inline too, straight off the
     row. A mode that must beat inline styles has one way to do it, and the
     alternative is a specificity race re-opened by the next rule anybody
     adds. Same reasoning as `body.has-cursor`. */
  /* ── 3. the deck, which is the default ─────────────────────────────
     The same cards. `!important` is the correct tool and not a shortcut:
     canvas.js writes `transform` to `.world` as an INLINE style on every
     frame, and the card's left/top/width are inline too, straight off the
     row. A mode that must beat inline styles has one way to do it, and the
     alternative is a specificity race re-opened by the next rule anybody
     adds. Same reasoning as `body.has-cursor`.

     EVERY SELECTOR HERE CARRIES `:not(.is-board)`, and that is load-bearing
     rather than tidy. The first cut of this wrote the deck unconditionally
     and then had a second block "putting the canvas back" for the board —
     which restored `position: absolute` and forgot left, top, width, height
     and min-height. They were still `auto !important`, so every absolutely
     positioned card resolved to its static position and THE WHOLE BOARD
     PILED UP IN THE TOP-LEFT CORNER. Tapping Board, which is half the
     navigation on a phone, gave you one heap of cards on an empty grid.

     It survived my own check because I verified the cards were ON SCREEN
     and never that they were in DIFFERENT PLACES; `new Set(rects).size === 1`
     is what catches it. A mode you have to UNDO in a second block is a list
     of properties somebody will fail to keep in step. A mode that simply
     stops applying cannot be got wrong. */
  body.canvas-page:not(.is-board) .viewport {
    overflow-y: auto;
    overflow-x: hidden;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-y: contain;
    touch-action: pan-y;
    cursor: default;
    /* No grid behind a list. The grid exists to make a surface legible as a
       surface you can pan; a scrolling column is not one. */
    background-image: none;
  }
  body.canvas-page:not(.is-board) .world {
    position: static !important;
    transform: none !important;
    will-change: auto;
    display: flex;
    flex-direction: column;
    gap: 12px;
    padding:
      calc(84px + env(safe-area-inset-top))
      calc(14px + env(safe-area-inset-right))
      calc(104px + env(safe-area-inset-bottom))
      calc(14px + env(safe-area-inset-left));
  }
  body.canvas-page:not(.is-board) .card {
    /* Capped and centred. Full width is right on a 375px phone and wrong the
       moment the screen is wide: in landscape the deck was drawing 784px-wide
       cards, which is a line length nobody reads comfortably and the reason
       this stylesheet keeps running text near 65 characters everywhere else.
       `align-self` rather than a margin, because the deck is a flex column. */
    width: min(100%, 620px) !important;
    align-self: center;
    position: static !important;
    left: auto !important;
    top: auto !important;
    height: auto !important;
    min-height: 0 !important;
    /* Order is set by JS from data-stage: what you are doing first, what you
       decided against last. Flex `order` rather than moving nodes, so the DOM
       the panel and Find both walk is never rewritten underneath them. */
  }
  /* Resizing is meaningless in a list, and a grip that does nothing is worse
     than no grip. */
  body.canvas-page:not(.is-board) .card .grip { display: none; }
  /* The zoomer belongs to a surface that pans. In the deck there is nothing
     to zoom and it would sit on top of the tab bar. */
  body.canvas-page:not(.is-board) .zoomer { display: none; }

  /* ── 4. the board ──────────────────────────────────────────────────
     Nothing to declare. Every rule above stops matching, so the canvas is
     exactly what the rest of this stylesheet already says it is — the inline
     left/top/width off each row included. The only thing the board needs on
     a phone is somewhere to sit that is not under the tab bar. */
  body.canvas-page.is-board .zoomer {
    bottom: calc(78px + env(safe-area-inset-bottom));
    left: calc(10px + env(safe-area-inset-left));
  }

  /* ── 5. the touch targets that were under the floor ────────────────
     28x27 against a 44px minimum. The visible pill keeps its size and the
     hit area grows underneath it, which is what the floor is actually for —
     inflating the drawing to satisfy a number is not the same thing. */
  .zoomer .btn {
    min-width: 44px;
    min-height: 44px;
  }
  .zoomer { padding: 4px 6px; }

  /* ── 6. every sheet opens from the bottom ──────────────────────────
     A centred box is a desktop dialog. On a phone the bottom is where the
     thumb is and where every native sheet comes from, and the New idea
     dialog — the thing this app is most useful for on a phone, catching an
     idea before it evaporates — is the one that benefits most.

     `bottom: var(--kb)` is the keyboard. iOS pins fixed elements to the
     LAYOUT viewport, so a sheet at `bottom: 0` sits BEHIND the keyboard the
     instant its field is focused. canvas.js measures the gap from
     visualViewport and publishes it; without that script the property is
     unset and the fallback 0px is exactly today's behaviour. */
  dialog.sheet {
    top: auto;
    bottom: var(--kb, 0px);
    left: 0; right: 0;
    width: 100%;
    max-width: none;
    /* MINUS --kb, not just `bottom: var(--kb)`. Lifting a sheet clear of the
       keyboard without also shortening it just slides the whole box upward:
       measured with a 336px keyboard, the New idea sheet went to y = -116 and
       the name field to y = -46 — you type the name of your idea into a field
       that is off the top of the screen. The card panel was worse, taking its
       own close button with it.

       A sheet has to fit in what is LEFT of the screen, which is what the
       keyboard leaves. Both halves or neither. */
    max-height: calc(86dvh - var(--kb, 0px));
    margin: 0;
    border-radius: 18px 18px 0 0;
    border-bottom: 0;
    /* `both` is load-bearing, not decoration. Without a fill mode this
       animation was leaving the sheet PERMANENTLY at its opening frame:
       measured at rest, 900ms after the 260ms animation, computed
       `top: 79.5px` + `height: 488px` = 568 = exactly the viewport, and a
       `transform: matrix(1,0,0,1,0,14)` sitting on top of it. Every number
       read back correct and the box was 14px lower than all of them.

       On a 568px screen that pushed the primary action — "Drop it on the
       canvas" — one pixel off the bottom. `both` pins the `to` state, so at
       rest the transform is none and the sheet is where its own geometry
       says it is. */
    animation: sheet-up .26s cubic-bezier(.19, 1, .3, 1) both;
  }
  .sheet-body {
    padding: 18px 18px calc(18px + env(safe-area-inset-bottom));
    overflow-y: auto;
  }
  .sheet-foot { padding: 13px 18px calc(13px + env(safe-area-inset-bottom)); }
  /* Anything in a sheet you press: 44 or more, without inflating the type. */
  .sheet-foot .btn, .sheet .btn { min-height: 44px; }
  /* The colour swatches were 30px. Unlike a tick box, a swatch is a thing you
     aim at BY its size and colour, so here the drawing genuinely should grow
     rather than hide a bigger target under a small circle. 38px plus a 3px
     ring of slop is 44, and the gap widens to 10 so the slop of one swatch
     never reaches its neighbour — an invisible target that overlaps the next
     one is worse than a small one, because it picks the wrong colour. */
  .swatches { gap: 10px; }
  .swatch { width: 38px; height: 38px; min-height: 38px; position: relative; }
  .swatch::after { content: ''; position: absolute; inset: -3px; border-radius: 50%; }
  .sheet input[type=text], .sheet input:not([type=hidden]), .sheet textarea, .sheet select { min-height: 46px; }

  /* The panel's keyboard lift belongs with the rule that MAKES it a bottom
     sheet, which is the 640px block, not this one. Applied here it also hit
     641-767px, where the panel still has its desktop `top`, so solving from
     both edges clamped it to a fixed 700px box whatever it contained. Moved
     down into the 640px block below. */

  /* ── 7. a sheet's primary action must be ON SCREEN ─────────────────
     Measured at 320x568: the New idea sheet holds 558px of content in 488px,
     so "Drop it on the canvas" opened 2px above the fold, and More's Close
     needed 96px of scroll. The dialog scrolls, so both were reachable — by
     someone who thought to try, on a sheet with no cue that there was more.

     The body scrolls and the footer does not. A primary action that is always
     the last thing on screen is worth more than a sheet that scrolls as one
     piece, especially on the short screens where this actually happens. */
  .sheet-body { max-height: calc(86dvh - var(--kb, 0px) - 8px); }
  .sheet-foot {
    position: sticky;
    bottom: 0;
    background: var(--bd-card);
    border-top: 1px solid var(--bd-rule);
  }

  /* ── 8. the menus hung off the LEFT edge ───────────────────────────
     `.pop` is 280px wide and hangs from its trigger's right edge. The views
     button is not AT the right edge — Share and the avatar are to its right —
     so at 320px the menu ran to x = -87 and a third of it, labels included,
     was off the screen. menu.js clamps correctly, but only on the portalled
     path, and `.bar-right` is deliberately not a scroll container so it is
     never portalled.

     `position: fixed` is NOT available here: `.bar-right` is backdrop-filtered
     and would become the containing block, which this file has already been
     caught by once. So the menu is pinned to the bar's own right edge with a
     width that cannot exceed the screen.

     `.bar-right .menu { position: static }` is the half that actually does
     it. `right: 0` alone still measured from the BUTTON's right edge, because
     `.menu` is the offset parent — the views button sits at x 190-246 with
     Share and the avatar to its right, so a 300px menu hanging off 246 still
     started at -54. Dropping the wrapper out of the positioning chain makes
     `.bar-right` the offset parent, which is the edge that is actually near
     the screen. It also lines all three menus up with each other. */
  .bar-right .menu { position: static; }
  .bar-right .pop {
    right: 0;
    left: auto;
    width: min(300px, calc(100vw - 20px));
    min-width: 0;
    max-height: calc(100dvh - 150px);
    overflow-y: auto;
    overscroll-behavior: contain;
  }
  /* 10.5px labels on the one control set a thumb aims at. */
  .tab-l { font-size: 11px; }

  /* ── 16px is not a type decision, it is a zoom decision ────────────
     iOS Safari AUTO-ZOOMS the page whenever a focused field is under 16px,
     and does not zoom back out. On the canvas, where the viewport is
     `user-scalable=no`, the page is then stuck magnified until a reload.

     Fixing this field by field is how it comes back: sign-in was 15px, the
     assistant 14.5px, the guest's own name field 13px, and each was found
     separately. One floor across every field there is, so a new input cannot
     reintroduce it. It only raises — anything already 16 or more is left
     alone.

     `!important` because the fields that most need it each have their own
     class rule — `.chit-in` at 14.5px, `.bd-write` at 15px — and a bare
     element selector loses to every one of them. This is a mode, in the same
     sense `body.has-cursor` is: while the screen is this small, no rule
     anywhere may put a field below the threshold that magnifies the page. */
  input:not([type=checkbox]):not([type=radio]):not([type=hidden]),
  textarea,
  select { font-size: max(16px, 1em) !important; }

  /* ── 9. a landscape phone is 375px tall ────────────────────────────
     The deck's 84px top and 104px bottom padding is half of that. The bars
     still have to be cleared, so the padding shrinks to exactly what clears
     them and the cards get the rest. */
  @media (max-height: 500px) {
    body.canvas-page:not(.is-board) .world {
      padding-top: calc(72px + env(safe-area-inset-top));
      padding-bottom: calc(80px + env(safe-area-inset-bottom));
      gap: 9px;
    }
    .tabs { padding: 3px 6px calc(3px + env(safe-area-inset-bottom)); }
    .tab { min-height: 44px; }
    .tab-new { width: 48px; height: 48px; min-height: 48px; margin-top: -12px; }
    .tab-new svg { width: 22px; height: 22px; }
  }

  /* ── 10. the empty state clears the bars at both ends ──────────────── */
  .blank {
    top: calc(96px + env(safe-area-inset-top));
    bottom: calc(96px + env(safe-area-inset-bottom));
  }
}

/* ── the More sheet ───────────────────────────────────────────────────
   A drawer is the wrong answer for primary navigation and the right one for
   everything else, and that is exactly the split: the five tabs are what a
   phone opens this app to do, and these are the ones you go looking for.

   It holds no state. Every row is an href to a route that already opens as a
   panel through sheet.js, so there is no second copy of anything to keep in
   step with the toolbar. */
.more-grip {
  width: 38px; height: 4px;
  margin: 0 auto 14px;
  border-radius: 99px;
  background: var(--bd-rule);
}
.more-list { display: flex; flex-direction: column; gap: 2px; margin: 14px 0 4px; }
.more-i {
  display: flex;
  align-items: center;
  gap: 13px;
  min-height: 52px;
  padding: 8px 10px;
  border-radius: 11px;
  color: inherit;
  text-decoration: none;
  -webkit-tap-highlight-color: transparent;
}
.more-i:active { background: var(--bd-sunk); }
.more-i svg { flex: none; width: 20px; height: 20px; color: var(--bd-faint); }
.more-i span { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.more-i b { font-size: 14.5px; font-weight: 600; }
.more-i i { font-size: 12px; font-style: normal; color: var(--bd-faint); line-height: 1.35; }
.more-sep { height: 1px; margin: 9px 10px; background: var(--bd-rule); }
/* Sticky, for the same reason `.sheet-foot` is: at 320x568 the seven rows are
   taller than the sheet, so Close sat 48px below the fold on the one control
   whose entire job is getting out. */
.more-x {
  position: sticky;
  bottom: 0;
  width: 100%;
  min-height: 48px;
  margin-top: 6px;
  background: var(--bd-card);
  border: 1px solid var(--bd-rule);
  border-radius: 12px;
  background: none;
  font: inherit;
  font-size: 14px;
  color: inherit;
  cursor: pointer;
}
.more-x:active { background: var(--bd-sunk); }

/* The three dots FAN APART on press, because that is what the control does:
   one thing opens into several. The icon animates its own mechanism, which
   is the rule every other icon in this file follows. */
/* Drawn heavier than the other icons on purpose. A dot is a round cap on a
   zero-length path, so at the shared 1.7 stroke these were three 1.7px specks
   sitting beside icons made of 22px strokes, and the More tab read as the
   faded one. Weight is matched by AREA here, not by stroke number. */
.i-pip { stroke-width: 2.6; transition: transform .22s cubic-bezier(.2,.9,.3,1); }
.tab:active .i-p1 { transform: translateX(-2.5px); }
.tab:active .i-p3 { transform: translateX(2.5px); }

@media (prefers-reduced-motion: reduce) {
  .i-pip, .tab, .tab-new { transition: none !important; }
  .tab-new:active { transform: none; }
}

/* ── opacity only, no transform ────────────────────────────────────────
   This animated `translateY(14px) -> none` and the sheet was measured AT
   REST, 900ms after a 260ms animation, still carrying
   `transform: matrix(1,0,0,1,0,14)`. Adding `both` did not help, so it was
   never reaching its end state at all — it was parked on the opening frame.

   The cost was not cosmetic: at 320x568 that 14px pushed "Drop it on the
   canvas" one pixel below the fold, on a sheet whose computed `top` and
   `height` added up to exactly the viewport. Every number read back correct.

   A slide is worth very little and a permanently displaced sheet is worth a
   lot less than nothing, so the motion is opacity alone. If the slide is ever
   wanted back, animate a WRAPPER inside the dialog — never the box whose own
   geometry is holding the sheet against the bottom of the screen. */
@keyframes sheet-up {
  from { opacity: .35; }
  to   { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  dialog.sheet { animation: none; }
}

/* The assistant's send button. Hidden wherever there is a real Enter key to
   press — on a phone the only affordance was a return glyph, which is a
   symbol for a key that keyboard does not have. */
.chit-send { display: none; }
@media (max-width: 767px), (max-height: 500px) and (max-width: 950px) {
  .chit-send {
    display: grid;
    place-items: center;
    flex: none;
    width: 44px;
    height: 44px;
    margin: -6px -6px -6px 0;
    border: 0;
    border-radius: 50%;
    background: none;
    color: var(--bd-brass);
    cursor: pointer;
  }
  .chit-send:active { background: var(--bd-sunk); }
  .chit-hint { display: none; }
}

/* Ask's close. It sits with the window rather than in a corner of the scrim,
   so it is where the thumb already is when the window is what you are
   looking at. */
.chit-x {
  justify-self: end;
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  margin-bottom: 6px;
  border: 0;
  border-radius: 50%;
  background: color-mix(in srgb, var(--bd-card) 82%, transparent);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  color: var(--bd-faint);
  cursor: pointer;
}
.chit-x:hover, .chit-x:focus-visible { color: var(--bd-ink); }
@media (pointer: coarse), (max-width: 767px) {
  .chit-x { width: 44px; height: 44px; }
}

/* A step that was decided against. Kept and shown — this app keeps decisions —
   but it is not a control: no checkbox, nothing to press, and it says what it
   is rather than looking like a live row that happens to be greyer. */
.step-row.gone { opacity: .62; }
.step-label.off { display: flex; align-items: flex-start; gap: 9px; flex: 1; }
.step-x { flex: none; width: 18px; text-align: center; color: var(--bd-faint); line-height: 1.5; }
.step-note {
  display: block;
  margin-top: 2px;
  font-size: 11.5px;
  font-style: normal;
  color: var(--bd-faint);
}

/* A step can be several lines — the writing line is a textarea with
   shift-enter for a new line, and the column is plain TEXT with no limit. It
   was then rendered with the newlines collapsed, so three lines came back as
   one run-together sentence: "QA multiline A QA multiline B QA multiline C".

   `pre-line` keeps the breaks the person typed and still collapses runs of
   spaces, which is what you want for text that was typed rather than laid
   out. The CARD face is deliberately not included — there a step is one line
   with an ellipsis, because a card that reflows to four lines because one
   step is long stops being scannable. */
.bd-step-label, .step-label > span { white-space: pre-line; }

/* ── who did it ────────────────────────────────────────────────────────
   The join door asks a stranger for a name and a colour "so whoever owns it
   can see what you changed", and then promises "anything you tick or write
   will carry this colour on the board". Both were true of the database —
   `guest_id` and `done_by_guest` have been written since guests existed —
   and neither was true of anything a person could look at. A promise kept
   only in a column nobody reads is not kept.

   Quiet on purpose: this is an annotation on a fact, not the fact. A history
   where the actor shouts louder than what happened is a changelog. */
.by {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  margin-left: 8px;
  font-size: 11.5px;
  font-style: normal;
  color: var(--bd-faint);
  white-space: nowrap;
}
.by-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  /* `.sw-*` declares --sw and nothing else; every element that wants to BE
     the colour has to spend it. */
  background: var(--sw, var(--bd-faint));
}
/* A guest is not a member, and the difference matters to the person reading:
   one is somebody with an account, the other is whoever was holding a link
   and typed a name. The chip stays quiet either way; the guest's carries the
   colour they picked, which is the only thing distinguishing them. */
.by-guest { font-style: normal; }
/* `inline-block`, not `block`, and `width: 100%` to still get its own line.
   `text-decoration` PROPAGATES to in-flow descendants and a child cannot
   refuse it — setting `text-decoration: none` on this element reads back as
   `none` in getComputedStyle and the line is still painted through it, which
   is exactly what a finished step did to the guest's name. An atomic
   inline-level box is not in that flow, so the decoration stops at it.

   The step being finished is a fact about the step. Who finished it is a fact
   about the step too, and striking it out says it was undone. */
.step-label .by { display: inline-block; width: 100%; margin: 2px 0 0; }

/* A step finished by a guest is ticked in THEIR colour — the tick is the
   thing they touched, so it is the honest place for the colour to land.

   NOT `accent-color`: `.step-check` is `appearance: none` with a hand-drawn
   box, so the UA accent has nothing to tint and the rule read back as a real
   plum value while painting patina like every other tick. The custom
   background is what has to change. */
.step-label[class*="sw-"] .step-check:checked {
  background: var(--sw);
  border-color: var(--sw);
}

/* The way to the other thing you might be here to do. Quiet — it is the
   second question, not the first — but present, which it was not. */
.dk-alt { margin: 14px 0 0; font-size: 12.5px; color: var(--dk-faint); text-align: center; }

/* The password rule, said before it is broken rather than after. A field that
   only tells you its constraint once you have failed it is a field that makes
   people feel stupid on the one screen where they have not used the app yet. */
.dk-hint {
  display: block;
  margin-top: 6px;
  font-size: 11.5px;
  font-style: normal;
  color: var(--dk-faint);
}

/* ══════════════════════════════════════════════════════════════════════
   THE PUBLIC FRONT — brassdeck.com/
   Replaces the thin page written only to satisfy OAuth's "publicly
   accessible home page" rule. Ported from scalewiseva.com/brassdeck when
   the domain was bought, so the product's front door is on the product's
   own domain rather than a path on the agency's.

   ── WHY EVERY CLASS HERE IS PREFIXED ─────────────────────────────────
   The marketing page was written as a standalone stylesheet with plain
   names, and EIGHT of them already mean something in this file: card,
   tabs, btn, bar, done, sheet, rows and field. .card is a project card
   and .tabs is the phone's tab bar. Merging unprefixed would not have
   looked broken in this file; it would have broken the canvas.

   ── AND WHY THE TOKENS ARE ON .hm, NOT :root ─────────────────────────
   A second :root block is the exact mistake the top of this file exists
   to prevent. These tokens describe one page and are scoped to it, so
   nothing here can reach the app.

   Type is SELF-HOSTED below. The CSP is deliberately strict
   (style-src-elem 'self', font-src 'self') and widening it so a marketing
   page can use a font CDN would spend a real security property on
   decoration. Both faces are OFL, so serving them from here is allowed.
   Instrument Sans is variable: one 30KB file covers 400 to 600.
   ══════════════════════════════════════════════════════════════════════ */
@font-face {
  font-family: 'Instrument Sans';
  src: url('/assets/fonts/instrument-sans.woff2') format('woff2');
  font-weight: 400 600;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Fragment Mono';
  src: url('/assets/fonts/fragment-mono.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* Full bleed. Same escape the sign-in desk uses two thousand lines up. */
.page:has(.hm) { max-width: none; margin: 0; padding: 0; }
body:has(.hm) { background: #F0EAD9; }
.hm { min-height: 100dvh; }
/* `:where()` contributes ZERO specificity, so this lands at (0,0,1) and any
   class rule beats it. Written as `.hm a` it was (0,1,1) and outranked
   `.hm-btn-1 { color: … }` at (0,1,0) — which painted the primary buttons'
   text in the page's near-black ink on their near-black fill. They rendered
   as solid black rectangles with the label invisible inside them, on every
   call to action on the page. The standalone sheet had this as a bare `a`
   at (0,0,1); scoping it to the page is what raised it. */
:where(.hm) a { color: inherit; text-decoration: none; }
.hm img { display: block; max-width: 100%; }
.hm *, .hm *::before, .hm *::after { box-sizing: border-box; }
/* ══════════════════════════════════════════════════════════════════════
   THE SYSTEM IS MEASURED, THE PAGE IS NOT COPIED.

   Every metric below was read off a reference site's computed styles
   rather than guessed — 66/1.10/-0.06em display, 20/1.40/300 body, an
   80px section rhythm, 20px slab radius, a 1280 container, four colours
   and no more. Those numbers are why a page reads as designed.

   Everything ABOVE the numbers is deliberately not theirs. The reference
   is saturated-first: one loud ground, everything else an interruption.
   This is PAPER-first, because the product is paper on a desk, and the
   saturated fields are the punctuation. Different face, different hero
   shape, different section order, and a torn edge rather than a scallop.
   ══════════════════════════════════════════════════════════════════════ */
.hm {
  /* These lived in the marketing sheet's body{} rule, which was dropped on
     the way in because this file's body{} belongs to the app. Without them
     the page silently inherited the app's system stack and Instrument Sans
     never loaded at all -- the face was registered and nothing referenced
     it, which reads as "the font did not download" and is not that. */
  font-family: var(--sans);
  background: var(--paper);
  color: var(--ink);
  -webkit-font-smoothing: antialiased;

  --paper:#F0EAD9;      /* the default ground                        */
  --ink:#17130C;        /* type, and the second full-bleed field     */
  --field:#1F5E4E;      /* patina — punctuation                      */
  --slab:#FDFBF6;       /* the floating slab                         */
  --brass:#C08A2E;
  --ink-2:#544B3C; --ink-3:#7C7261;
  --on-field:#EFE7D4; --on-field-2:rgba(239,231,212,.74);
  --on-ink:#F0EAD9; --on-ink-2:rgba(240,234,217,.7);
  --sans:"Instrument Sans",-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
  --mono:"Fragment Mono",ui-monospace,Menlo,monospace;
  --pad:80px;                       /* the section rhythm, exactly    */
  --gut:clamp(20px,4vw,48px);
}
.hm-wrap {max-width:1280px;margin:0 auto;padding-inline:var(--gut)}
.hm-narrow {max-width:1120px;margin:0 auto;padding-inline:var(--gut)}

/* the measured scale */
.hm h1 {font-size:clamp(40px,5.4vw,72px);font-weight:500;line-height:1.06;letter-spacing:-.06em}
.hm h2 {font-size:clamp(32px,3.9vw,52px);font-weight:500;line-height:1.10;letter-spacing:-.06em}
.hm h3 {font-size:19px;font-weight:600;letter-spacing:-.035em}
.hm-eyebrow {display:block;font-size:14px;font-weight:500;letter-spacing:-.045em;margin-bottom:22px;color:var(--ink-3)}
.hm-lede {font-size:20px;font-weight:400;line-height:1.40;color:var(--ink-2)}
.hm p {font-size:17px;font-weight:400;line-height:1.48;color:var(--ink-2)}
.hm-tag {display:block;font-family:var(--mono);font-size:12px;color:var(--field);margin-bottom:11px}

.hm-btn {display:inline-flex;align-items:center;gap:8px;height:48px;padding:0 18px;
  border-radius:12px;font-size:14px;font-weight:500;letter-spacing:-.02em;transition:filter .15s}
.hm-btn:hover {filter:brightness(1.09)}
.hm-btn-1 {background:var(--ink);color:var(--paper)}
.hm-btn-2 {background:rgba(23,19,12,.07);color:var(--ink)}
.hm-on-dark .hm-btn-1 {background:var(--on-field);color:var(--ink)}
.hm-on-dark .hm-btn-2 {background:rgba(239,231,212,.15);color:var(--on-field)}
.hm-sec {padding-block:var(--pad)}

/* ── nav sits on PAPER, not on a colour field ─────────────────────── */
.hm-nav {display:flex;align-items:center;gap:26px;max-width:1440px;margin:0 auto;padding:22px var(--gut)}
.hm-brand {display:flex;align-items:center;gap:9px;font-size:21px;font-weight:600;
  letter-spacing:-.05em;margin-right:auto}
.hm-nav a.hm-l {font-size:15px;letter-spacing:-.02em;color:var(--ink-2)}
.hm-nav .hm-btn {height:40px}

/* ── HERO: one full-width headline, product full-bleed beneath ─────
   Not copy-left / device-right. The board is the widest thing this
   product has and it gets the whole page width to be readable in. */
.hm-hero {padding-top:44px}
.hm-hero h1 {max-width:17ch;margin-bottom:26px}
.hm-hero-row {display:flex;justify-content:space-between;align-items:flex-end;gap:40px;flex-wrap:wrap}
.hm-hero .hm-lede {max-width:38ch}
.hm-hero-cta {display:flex;gap:12px;flex-wrap:wrap;max-width:100%}
.hm-hero-band {margin-top:56px;height:clamp(240px,42vw,520px);overflow:hidden;background:#E8E0D2}
.hm-hero-band img {width:100%;height:100%;object-fit:cover;object-position:center 26%}
.hm-hero-note {display:flex;gap:28px;flex-wrap:wrap;padding-top:16px;
  font-family:var(--mono);font-size:12px;color:var(--ink-3)}
.hm-hero-note b {color:var(--ink);font-weight:400}

/* ── a saturated field, used as punctuation ───────────────────────── */
.hm-field {background:var(--field);color:var(--on-field)}
.hm-field .hm-lede, .hm-field p {color:var(--on-field-2)}
.hm-field .hm-eyebrow {color:rgba(239,231,212,.6)}
.hm-field .hm-tag {color:#9CCBB6}
.hm-clock {display:grid;grid-template-columns:minmax(0,1fr) minmax(0,.86fr);
  gap:clamp(32px,5vw,72px);align-items:center}
.hm-clock h2 {max-width:14ch;margin-bottom:20px}
.hm-clock .hm-lede + .hm-lede {margin-top:16px}
.hm-rows {border-top:1px solid rgba(239,231,212,.26)}
.hm-rows div {display:flex;justify-content:space-between;align-items:baseline;padding:16px 0;
  border-bottom:1px solid rgba(239,231,212,.13);font-size:18px;letter-spacing:-.03em}
.hm-rows span:last-child {font-family:var(--mono);font-size:12px}
.hm-rows .hm-y span:last-child {color:#E6C77E}
.hm-rows .hm-n span {color:rgba(239,231,212,.46)}

/* ── the slab, on paper ───────────────────────────────────────────── */
.hm-slab {background:var(--slab);border-radius:20px;padding:var(--pad) clamp(24px,4vw,56px)}
.hm-slab h2 {max-width:17ch;margin-bottom:18px}
.hm-slab .hm-lede {max-width:44ch}
.hm-anat {display:grid;grid-template-columns:minmax(0,.85fr) minmax(0,1.15fr);
  gap:clamp(32px,5vw,64px);align-items:center;margin-top:56px}
.hm-card {position:relative;background:#FFFFFF;border-radius:14px;padding:24px 26px;max-width:360px;
  box-shadow:0 24px 48px -24px rgba(23,19,12,.28)}
.hm-card-n {font-size:20px;font-weight:600;letter-spacing:-.04em;margin-bottom:14px}
.hm-step {display:flex;gap:10px;font-size:15px;line-height:1.5;color:var(--ink-2);padding:5px 0}
.hm-step i {flex:none;width:13px;height:13px;margin-top:4px;border-radius:3px;
  box-shadow:0 0 0 1.5px var(--ink-3) inset}
.hm-step.hm-done {color:var(--ink-3);text-decoration:line-through}
.hm-step.hm-done i {background:var(--field);box-shadow:none}
.hm-bar {height:3px;background:rgba(23,19,12,.12);margin:14px 0 11px;overflow:hidden}
.hm-bar span {display:block;height:100%;width:33%;background:var(--field)}
.hm-card-f {display:flex;justify-content:space-between;align-items:baseline;
  font-family:var(--mono);font-size:11px;color:var(--field)}
.hm-card-f em {font-style:normal;color:var(--ink-3)}
.hm-notes {border-top:1px solid rgba(23,19,12,.16)}
.hm-notes > div {display:grid;grid-template-columns:auto 1fr;gap:18px;padding:18px 0;
  border-bottom:1px solid rgba(23,19,12,.1)}
.hm-notes .hm-n {font-family:var(--mono);font-size:12px;color:var(--field);padding-top:3px}
.hm-notes h3 {margin-bottom:5px}

/* ── stages: hairline dividers, no boxes anywhere ─────────────────── */
.hm-tabs {display:grid;grid-template-columns:repeat(6,1fr);border-top:1px solid rgba(23,19,12,.2);margin-top:52px}
.hm-tabs > div {padding:20px 18px 24px 0;border-left:1px solid rgba(23,19,12,.11)}
.hm-tabs > div:first-child {border-left:0}
.hm-tabs > div:not(:first-child) {padding-left:18px}
.hm-tabs h3 {margin-bottom:7px}
.hm-tabs p {font-size:14px;line-height:1.45;color:var(--ink-3)}
.hm-tabs .hm-exit .hm-tag {color:var(--ink-3)}
.hm-after {margin-top:26px;max-width:60ch;font-size:15px;color:var(--ink-3)}

/* ── the torn edge: paper, not a scallop ──────────────────────────── */
.hm-tear {display:block;width:100%;height:44px;background:var(--paper)}

/* ── the ink field: views + phone ─────────────────────────────────── */
.hm-dark {background:var(--ink);color:var(--on-ink)}
.hm-dark .hm-lede, .hm-dark p {color:var(--on-ink-2)}
.hm-dark .hm-eyebrow {color:rgba(240,234,217,.55)}
.hm-dark .hm-tag {color:#E6C77E}
.hm-stack {margin-top:52px;position:relative;padding-top:54px;padding-right:clamp(0px,7vw,90px)}
.hm-sheet {border-radius:14px;overflow:hidden;box-shadow:0 30px 60px -28px rgba(0,0,0,.7)}
.hm-sheet-f {position:relative;z-index:3;max-height:440px}
.hm-sheet-b {position:absolute;right:0;width:min(84%,860px);max-height:280px;opacity:.5}
.hm-sheet-b1 {top:27px;z-index:2;opacity:.72}
.hm-sheet-b2 {top:0}
.hm-legend {display:grid;grid-template-columns:repeat(3,1fr);gap:clamp(20px,3vw,44px);margin-top:44px}
.hm-legend h3 {margin-bottom:6px}
.hm-legend p {font-size:15px;color:var(--on-ink-2)}
.hm-pocket {display:grid;grid-template-columns:minmax(0,1fr) auto;
  gap:clamp(32px,6vw,80px);align-items:center;margin-top:calc(var(--pad) * 1.4)}
.hm-pocket h2 {max-width:14ch;margin-bottom:18px}
.hm-pocket .hm-lede {max-width:40ch}
.hm-plist {list-style:none;margin-top:26px;display:flex;flex-direction:column;gap:12px}
.hm-plist li {display:grid;grid-template-columns:auto 1fr;gap:12px;font-size:16px;color:var(--on-ink-2)}
.hm-plist b {font-family:var(--mono);font-size:12px;color:#E6C77E;font-weight:400;padding-top:3px}
.hm-phone {width:288px;flex:none;border-radius:44px;padding:11px;background:#080604;
  box-shadow:0 2px 1px rgba(255,245,225,.16) inset,0 44px 76px -28px rgba(0,0,0,.8)}
.hm-phone-scr {position:relative;border-radius:34px;overflow:hidden;background:#E8E0D2}
.hm-phone-bar {position:relative;display:flex;align-items:center;justify-content:space-between;
  height:36px;padding:0 20px;background:#E8E0D2;color:#1B1409;font-size:12px;font-weight:600}
.hm-phone-bar::after {content:"";position:absolute;top:7px;left:50%;transform:translateX(-50%);
  width:78px;height:21px;border-radius:99px;background:#080604}

/* ── close ────────────────────────────────────────────────────────── */
.hm-close {text-align:center}
.hm-close h2 {max-width:18ch;margin:0 auto 18px}
.hm-close .hm-lede {max-width:46ch;margin:0 auto 32px}
.hm-close .hm-hero-cta {justify-content:center}
.hm-foot {border-top:1px solid rgba(23,19,12,.14);padding-block:30px;
  display:flex;flex-wrap:wrap;gap:14px 26px;align-items:center;font-size:14px;color:var(--ink-3)}
.hm-foot .hm-brand {font-size:17px;color:var(--ink)}
.hm-foot small {width:100%;font-size:13px;color:var(--ink-3)}

@media (max-width:980px){
.hm-clock, .hm-anat, .hm-pocket {grid-template-columns:1fr}
.hm-card {max-width:none}
.hm-tabs {grid-template-columns:repeat(3,1fr)}
.hm-tabs > div:nth-child(4) {border-left:0}
.hm-stack {padding-top:0;padding-right:0}
.hm-sheet-b {display:none} .hm-sheet-f{max-height:none}
.hm-legend {grid-template-columns:1fr}
.hm-pocket-art {order:-1}
}
@media (max-width:768px){
.hm-nav .hm-btn {height:44px}
.hm-foot a, .hm-foot .hm-brand {min-height:44px;display:inline-flex;align-items:center}
}
@media (max-width:620px){
:root {--pad:56px}
.hm-tabs {grid-template-columns:1fr 1fr}
.hm-tabs > div {border-left:0;border-top:1px solid rgba(23,19,12,.11);padding-left:0}
.hm-tabs > div:not(:first-child) {padding-left:0}
.hm-tabs > div:nth-child(-n+2) {border-top:0}
.hm-nav a.hm-l {display:none}
}


/* Privacy and Terms. Ordinary document pages — they are read once, carefully,
   usually by someone deciding whether to trust this, so the only thing that
   matters is that they are legible and unambiguous. */
.doc-p { margin: 0 0 12px; font-size: 14px; line-height: 1.62; color: var(--bd-soft); max-width: 64ch; }
.doc-p:last-child { margin-bottom: 0; }
.doc-list { margin: 0; padding-left: 18px; display: flex; flex-direction: column; gap: 11px; }
.doc-list li { font-size: 14px; line-height: 1.62; color: var(--bd-soft); max-width: 64ch; }
.doc-list b { color: var(--bd-ink); font-weight: 600; }

/* ══════════════════════════════════════════════════════════════════════
   THE OPERATOR SURFACE
   Deliberately plain. This is the one screen in the app that shows other
   people's data, and it should feel like a ledger rather than like the
   product — nothing here is meant to be pleasant to spend time in. Same
   tokens, no card shadows, no accents beyond what carries meaning. */
.rt-door { max-width: 28rem; }
.rt-flash {
  margin: 0 0 14px;
  padding: 10px 12px;
  border-radius: 9px;
  background: color-mix(in srgb, var(--bd-clay) 16%, transparent);
  font-size: 13.5px;
}
.rt-form { display: flex; flex-direction: column; gap: 14px; margin-top: 16px; }
.rt-form label { display: flex; flex-direction: column; gap: 6px; font-size: 12.5px; color: var(--bd-soft); }

/* Counts. Tabular figures so the column of numbers lines up and a change in
   magnitude reads as a change rather than as the layout shifting. */
.rt-stats {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(8.5rem, 1fr));
  gap: 14px 10px;
  margin-bottom: 12px;
}
.rt-stat { display: flex; flex-direction: column; gap: 1px; }
.rt-stat b { font-size: 20px; font-weight: 600; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.rt-stat i { font-size: 11.5px; font-style: normal; color: var(--bd-faint); }

.rt-table { display: flex; flex-direction: column; }
.rt-head, .rt-row {
  display: grid;
  grid-template-columns: 2fr 1fr 1fr 1.4fr 1fr auto;
  gap: 10px;
  align-items: center;
  padding: 9px 0;
  font-size: 13px;
  font-variant-numeric: tabular-nums;
}
.rt-head-4, .rt-row-4 { grid-template-columns: 1.4fr 2fr .7fr 1.2fr; }
.rt-head {
  font-size: 11px;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--bd-faint);
  border-bottom: 1px solid var(--bd-rule);
}
.rt-row + .rt-row { border-top: 1px solid var(--bd-rule); }
/* Suspended, or a removed member. Faded rather than struck through or red:
   this file's rule about not using red for a state that is not a failure
   applies to the operator's screen too. */
.rt-row.off { opacity: .5; }
.rt-name { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.rt-name b { font-weight: 600; }
.rt-name i, .rt-sub { font-size: 11.5px; font-style: normal; color: var(--bd-faint); }
.rt-mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; word-break: break-all; }
.rt-open { color: var(--bd-brass); text-decoration: none; font-size: 12.5px; }
.rt-open:hover, .rt-open:focus-visible { text-decoration: underline; }

.rt-line { display: flex; align-items: baseline; gap: 9px; padding: 6px 0; font-size: 13.5px; }
.rt-line + .rt-line { border-top: 1px solid var(--bd-rule); }
/* Filled means it happened: a step that is done, a link somebody opened. */
.rt-dot {
  flex: none;
  width: 7px; height: 7px;
  border-radius: 50%;
  border: 1.5px solid var(--bd-faint);
}
.rt-dot.on { background: var(--bd-patina); border-color: var(--bd-patina); }

.rt-actions { display: flex; align-items: flex-end; gap: 12px; flex-wrap: wrap; margin-bottom: 14px; }
.rt-actions label { display: flex; flex-direction: column; gap: 5px; font-size: 12px; color: var(--bd-soft); }

@media (max-width: 767px) {
  /* One column per row on a phone. A six-column ledger at 375px is unreadable
     whatever you do to the type, so the columns stack and the labels go. */
  .rt-head { display: none; }
  .rt-row, .rt-row-4 { grid-template-columns: 1fr; gap: 3px; padding: 12px 0; }
  .rt-stats { grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr)); }
}
