/* ==================================================================
   MOBILE FOUNDATION — Monway Team Platform
   ==================================================================
   Loaded AFTER styles.css, deliberately. styles.css is a desktop-first
   sheet with a phone/tablet section bolted on and a "design polish"
   layer appended after that; the two already fight each other on
   specificity (its own comments say so). Rather than interleave a third
   set of rules into that argument, everything the mobile experience
   needs lives here and wins by being last.

   The rule for anything added here: it must be a SYSTEM — a token, a
   primitive, or a behaviour that applies everywhere — never a patch for
   one page. Per-page fixes belong in the page's own section of
   styles.css, built on what this file defines.
   ================================================================== */


/* ==================================================================
   1. TOKENS
   ================================================================== */
:root{
  /* ---- touch ----
     44px is Apple's documented minimum, 48px Material's. Primary
     controls take 48; dense secondary controls keep their visual size
     and reach 44 through an invisible hit-slop (see section 4). */
  --tap:48px;
  --tap-min:44px;
  --tap-sm:40px;

  /* ---- spacing ----
     A 4px scale. Every gap, pad and margin added from here on picks a
     step instead of inventing a number — that is what stops the
     "awkward whitespace" this refactor exists to remove. */
  --sp-1:4px;  --sp-2:8px;  --sp-3:12px; --sp-4:16px;
  --sp-5:20px; --sp-6:24px; --sp-7:32px; --sp-8:40px;

  /* ---- radii ---- */
  --r-xs:8px; --r-md:12px; --r-lg:16px; --r-xl:22px; --r-full:999px;

  /* ---- safe area ----
     Read once here so no other rule has to repeat the env() dance.
     These are 0 on every screen without a notch or home indicator, so
     they cost nothing where they are not needed. NOTE: they only ever
     report a real value when the viewport meta carries
     `viewport-fit=cover` — without it iOS reports 0 and clips the UI. */
  --safe-t:env(safe-area-inset-top,0px);
  --safe-r:env(safe-area-inset-right,0px);
  --safe-b:env(safe-area-inset-bottom,0px);
  --safe-l:env(safe-area-inset-left,0px);

  /* ---- chrome heights ---- */
  --tabbar-h:56px;
  --tabbar-total:calc(var(--tabbar-h) + var(--safe-b));

  /* ---- motion ----
     Native UI moves fast and settles; it does not glide. 150-220ms with
     a decelerating curve is the whole vocabulary. */
  --ease-out:cubic-bezier(.2,.8,.3,1);
  --ease-spring:cubic-bezier(.34,1.3,.64,1);
  --t-fast:120ms;
  --t-base:180ms;
  --t-slow:260ms;

  /* ---- layering ----
     Named so a new overlay never has to guess a number. Matches the
     ladder styles.css already uses; the tab bar slots below every
     scrim so modals and the drawer cover it. */
  --z-tabbar:15;
  --z-scrim:20;
  --z-drawer:21;
  --z-modal:30;
  --z-nav:60;
  --z-pop:90;
  --z-toast:110;
  --z-lightbox:120;
}

/* ---- accessible accent ----
   The brand orange is legible on the dark theme and is not on the light one.
   Measured against the real tab-bar background (#F7F7F5 after compositing):

       #F97316  2.61     #EA580C  3.32     #C2410C  4.83     #9A3412  6.81

   so the light theme takes #C2410C, the first of them that clears AA's 4.5.
   --on-orange is the ink for text and icons sitting ON an orange fill, where
   white measures 2.80 and fails even the 3.0 bar for a graphic; #1A1207 — the
   same ink .btn already uses — measures 6.61. */
[data-theme="dark"]{  --accent-ink:var(--orange); }
[data-theme="light"]{ --accent-ink:#C2410C; }
:root{ --on-orange:#1A1207; }

/* The keyboard-aware viewport height, kept current by ui.ts from
   visualViewport. Falls back to the layout viewport when that API is
   missing, which is correct everywhere it is missing. */
:root{ --vvh:100dvh; --kb:0px; }


/* ==================================================================
   2. VIEWPORT — dynamic height and safe areas
   ==================================================================
   iOS Safari's URL bar makes 100vh taller than what is actually on
   screen, so a 100vh app has its last ~60px permanently under the
   chrome. 100dvh tracks the real visible height. */
html,body{ height:100%; }

html[data-vm="phone"] .app,
html[data-vm="tablet"] .app{ height:100dvh; }

/* The notch and the rounded corners eat into the left and right edges in
   landscape. Padding the shell, rather than each page, means no view has
   to know it is running on a notched device. */
html[data-vm="phone"] .app{
  padding-left:var(--safe-l);
  padding-right:var(--safe-r);
}


/* ==================================================================
   3. TOUCH BEHAVIOUR
   ================================================================== */

/* The 300ms tap delay is a legacy of double-tap-to-zoom. Declaring that
   these elements do not participate in gestures removes it outright. */
button, .btn, .icon-btn, .chip, .nav, .dtab, .tab, a, label,
input, select, textarea, [role="button"]{
  touch-action:manipulation;
}

/* Mobile browsers paint a grey box over anything tapped. We provide our
   own press feedback below, so theirs is noise on top of ours. */
html[data-vm="phone"] *{
  -webkit-tap-highlight-color:transparent;
}

/* Momentum scrolling, and a scroll that stops at its own edge instead of
   dragging the page (or the whole browser) behind it. Applied to the
   containers that actually scroll DOWN. */
.main, .drawer-body, .modal, .chat-msgs, #channelList, .sheet-body{
  -webkit-overflow-scrolling:touch;
  overscroll-behavior:contain;
}

/* The sideways ones get it on one axis, and it matters which.
   `overflow-x:auto` computes overflow-y to `auto` too, so a table strip is
   a vertical scroll container as well — one that never has anywhere to go.
   `overscroll-behavior:contain` on a scroll container with nowhere to go
   does not hand the wheel up to the page; it eats it. With the shorthand
   here, a trackpad scroll on Tasks moved the page about one notch, the
   table slid under the pointer, and everything stopped dead — on every
   page with a table, which is most of them.
   Contain the axis that actually scrolls; leave the other one alone. The
   phone rules further down always said `overscroll-behavior-x`; this is
   the same answer, applied where it was first needed. */
.table-wrap, .kanban, .feed{
  -webkit-overflow-scrolling:touch;
  overscroll-behavior-x:contain;
}

/* ---- press feedback ---------------------------------------------
   Hover cannot fire on a touch screen, so every :hover rule in
   styles.css is dead weight there — worse, tapping leaves the hover
   state stuck on until something else is tapped. Replace it with a
   press state that is instant, obvious, and lets go. */
/* NOT neutralised with `inherit`. An earlier version of this file reset the
   hover states here to `background:inherit;color:inherit`, on the theory that
   inherit means "back to normal". It does not — it means "take the parent's
   value", and measured on this app that was visibly wrong:

     .chip    active: orange on #1c1c1c   ->  transparent on #F2F4F7
     .nav     active: #1B1F26             ->  transparent
     .icon-btn        #9BA3B0             ->  #F2F4F7

   so a tapped chip lost its fill and a tapped nav row lost its selection.
   The reset was worse than the problem it was aimed at.

   Doing it properly would mean scoping all 83 of styles.css's :hover rules in
   `@media (hover:hover)`, and several of them share a selector list with
   :focus-within that touch still needs (see the block above this one) — so a
   blind transform would break them. Left for when those rules are next
   touched. What actually mattered — that a tap had no feedback at all, 83
   hover rules against 2 active ones — is fixed below. Sticky hover in this
   app is cosmetic and clears itself on the next tap. */

.btn, .icon-btn, .chip, .nav, .dtab, .tab, .kcard, .chan, .me-row{
  transition:transform var(--t-fast) var(--ease-out),
             opacity var(--t-fast) var(--ease-out),
             background-color var(--t-fast) var(--ease-out);
}
.btn:active, .icon-btn:active, .chip:active, .dtab:active, .tab:active{
  transform:scale(.96);
  opacity:.85;
}
/* Rows are wide: scaling one looks like the layout jumped. They dim
   and tint instead, the way a native list row does. */
.nav:active, .kcard:active, .chan:active, .me-row:active,
.clist-item:active, tr.clickable:active{
  background:var(--card2);
  opacity:.9;
  transform:none;
}
/* Someone who has asked their OS for less motion gets none of it. */
@media (prefers-reduced-motion:reduce){
  *, *::before, *::after{
    animation-duration:.01ms !important;
    animation-iteration-count:1 !important;
    transition-duration:.01ms !important;
    scroll-behavior:auto !important;
  }
}


/* ==================================================================
   4. TOUCH TARGETS
   ==================================================================
   Two strategies, on purpose.

   Primary controls GROW: a button, a form field or a nav row is a thing
   you aim at, and on a phone it should be as big as your thumb.

   Dense secondary controls KEEP THEIR SIZE and grow an invisible
   hit-slop instead. The chat action row, the reaction bar and the
   toolbar icon buttons are deliberately compact — inflating them to
   48px would wreck layouts that are correct. The pseudo-element below
   extends the tappable area past the visible edge, which is exactly
   what UIKit and Android do. */

/* ---- grow ---- */
html[data-vm="phone"] .btn{
  min-height:var(--tap);
  padding:0 var(--sp-4);
  display:inline-flex;
  align-items:center;
  justify-content:center;
  gap:var(--sp-2);
  font-size:15px;
  border-radius:var(--r-md);
}
html[data-vm="phone"] .btn.sm{ min-height:var(--tap-min); font-size:14px; padding:0 var(--sp-3); }
html[data-vm="phone"] .btn.wide{ width:100%; }

html[data-vm="phone"] input,
html[data-vm="phone"] select,
html[data-vm="phone"] textarea{
  min-height:var(--tap);
  border-radius:var(--r-md);
  padding:var(--sp-3) var(--sp-3);
  /* 16px is the threshold below which iOS zooms the page on focus.
     Anything smaller here is a guaranteed layout jolt. */
  font-size:16px;
}
html[data-vm="phone"] textarea{ min-height:96px; }
html[data-vm="phone"] select{ padding-right:var(--sp-6); }

html[data-vm="phone"] .nav{
  min-height:var(--tap);
  padding:0 var(--sp-3);
  font-size:15px;
  border-radius:var(--r-md);
}

html[data-vm="phone"] .dtab{
  min-height:var(--tap-min);
  padding:0 var(--sp-3);
  display:inline-flex;
  align-items:center;
  font-size:14px;
}

html[data-vm="phone"] .chip{
  min-height:var(--tap-sm);
  padding:0 var(--sp-4);
  display:inline-flex;
  align-items:center;
  font-size:13.5px;
  border-radius:var(--r-full);
}

html[data-vm="phone"] .ms-btn{
  min-height:var(--tap-min);
  border-radius:var(--r-md);
}

/* A checkbox at its native ~13px is the smallest target in the app. */
html[data-vm="phone"] input[type=checkbox],
html[data-vm="phone"] input[type=radio]{
  width:22px; height:22px;
  min-height:0;
  accent-color:var(--orange);
}
html[data-vm="phone"] .toggle{
  display:inline-flex;
  align-items:center;
  gap:var(--sp-2);
  min-height:var(--tap-min);
}

/* ---- hit-slop ---- */
html[data-vm="phone"] .icon-btn{
  position:relative;
  min-width:36px;
  min-height:36px;
}
html[data-vm="phone"] .icon-btn::after{
  content:"";
  position:absolute;
  /* centre a 44px box on the button whatever its visual size */
  top:50%; left:50%;
  width:max(100%, var(--tap-min));
  height:max(100%, var(--tap-min));
  transform:translate(-50%,-50%);
}

/* Icons inside buttons are the single most common alignment defect in
   this codebase: an <svg> is an inline element, so it sits on the text
   baseline and rides a few pixels low. Making every button a flex box
   and every icon a flex item centres them exactly, everywhere, once. */
.btn, .icon-btn, .chip, .dtab, .nav{
  display:inline-flex;
  align-items:center;
  justify-content:center;
  line-height:1;
}
.nav, .dtab{ justify-content:flex-start; }
.btn > svg, .icon-btn > svg, .chip > svg, .nav svg, .btn-ic{
  flex:none;
  display:block;
}


/* ==================================================================
   5. BOTTOM TAB BAR
   ==================================================================
   Fourteen nav items cannot be a tab bar, but the four people actually
   live in can. The bar carries those four plus "More", which opens the
   existing slide-over holding the full list — so nothing is lost and
   the common case stops costing two taps and an animation.

   Built by ui.ts from the real .nav buttons, so permissions, labels and
   unread badges stay defined in exactly one place. */
.tabbar{ display:none; }

html[data-vm="phone"] .tabbar{
  display:flex;
  /* styles.css styles the bare `nav` element as the sidebar's vertical list
     (flex-direction:column, flex:1, overflow:auto). That element selector
     reaches this <nav> too, and a class alone does not outrank a property
     the class never declares — so the tabs stacked on top of each other.
     Every one of those is restated here rather than left to be inherited. */
  flex-direction:row;
  gap:0;
  flex:none;
  overflow:visible;
  position:fixed;
  left:0; right:0; bottom:0;
  z-index:var(--z-tabbar);
  height:var(--tabbar-total);
  padding:0 var(--safe-r) var(--safe-b) var(--safe-l);
  background:color-mix(in srgb, var(--bg) 82%, transparent);
  backdrop-filter:saturate(180%) blur(20px);
  -webkit-backdrop-filter:saturate(180%) blur(20px);
  border-top:1px solid var(--line);
}

.tab{
  flex:1 1 0;
  min-width:0;
  display:flex;
  flex-direction:column;
  align-items:center;
  justify-content:center;
  gap:3px;
  background:none;
  border:0;
  padding:var(--sp-1) 2px 0;
  /* --tx3 measures 3.00 (light) and 3.76 (dark) against this bar; --tx2
     measures 5.91 and 7.37. A 10.5px label needs the second one. */
  color:var(--tx2);
  font-family:inherit;
  font-size:10.5px;
  font-weight:500;
  letter-spacing:.01em;
  cursor:pointer;
  position:relative;
  -webkit-tap-highlight-color:transparent;
}
.tab svg{
  width:23px; height:23px;
  fill:none;
  stroke:currentColor;
  stroke-width:1.9;
  stroke-linecap:round;
  stroke-linejoin:round;
}
.tab span{
  max-width:100%;
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
}
.tab.active{ color:var(--accent-ink); }
.tab.active svg{ stroke-width:2.2; }
.tab:active{ transform:scale(.92); opacity:1; }

/* The badge rides the icon, not the row — a count pushed into the label
   would shift the text and make the bar twitch as messages arrive. */
.tab .tab-badge{
  position:absolute;
  top:2px;
  left:calc(50% + 6px);
  min-width:17px;
  height:17px;
  padding:0 4px;
  border-radius:var(--r-full);
  /* The dark theme's --bad is a light red, which white sits on at 2.5:1.
     Darkening the fill rather than the text keeps it reading as an alert. */
  background:color-mix(in srgb, var(--bad) 82%, #000);
  color:#fff;
  font-size:10px;
  font-weight:700;
  font-style:normal;
  line-height:17px;
  text-align:center;
  border:2px solid var(--bg);
}
.tab .tab-badge.dot{
  min-width:9px; width:9px; height:9px;
  padding:0;
  top:5px;
}

/* Content must be able to scroll clear of the bar. The view's own bottom
   padding does it, so nothing needs a spacer element. */
html[data-vm="phone"] .view{
  padding-bottom:calc(var(--tabbar-total) + var(--sp-5));
}
/* Anything that floats near the bottom has to clear the bar too. */
html[data-vm="phone"] .bulkbar{
  bottom:calc(var(--tabbar-total) + var(--sp-3));
}
html[data-vm="phone"] .toast{
  bottom:calc(var(--tabbar-total) + var(--sp-3));
}
/* The slide-over nav covers the bar rather than stopping above it —
   it is the same navigation, expanded. */
html[data-vm="phone"] .sidebar{
  padding-top:calc(var(--sp-4) + var(--safe-t));
  padding-bottom:calc(var(--sp-3) + var(--safe-b));
}


/* ==================================================================
   6. PRIMITIVES — loading
   ==================================================================
   There was no loading vocabulary at all: every view painted blank and
   then jumped when its fetch landed. A skeleton holds the shape the
   content will take, so the layout never moves. */
.skeleton{
  position:relative;
  overflow:hidden;
  /* Tinted from the text colour, not set to a surface colour. --card2 on a
     --card background is a 4% difference: the placeholders inside a KPI tile
     were there, occupied the right space, and were invisible. Mixing against
     the text means a skeleton shows up on whatever it is sitting on. */
  background:color-mix(in srgb, var(--tx) 10%, transparent);
  border-radius:var(--r-xs);
}
.skeleton::after{
  content:"";
  position:absolute;
  inset:0;
  transform:translateX(-100%);
  background:linear-gradient(90deg,
    transparent,
    color-mix(in srgb, var(--tx) 7%, transparent),
    transparent);
  animation:shimmer 1.4s infinite;
}
@keyframes shimmer{ to{ transform:translateX(100%); } }

.sk-line{ height:11px; margin-bottom:var(--sp-2); }
.sk-line.w40{ width:40%; } .sk-line.w60{ width:60%; }
.sk-line.w80{ width:80%; } .sk-line.w100{ width:100%; }
.sk-title{ height:15px; width:55%; margin-bottom:var(--sp-3); }
.sk-circle{ border-radius:50%; width:38px; height:38px; flex:none; }
.sk-chip{ height:26px; width:74px; border-radius:var(--r-full); }

/* One row of a list, shaped like the real thing. */
.sk-row{
  display:flex;
  align-items:center;
  gap:var(--sp-3);
  padding:var(--sp-3) 0;
  border-bottom:1px solid var(--line);
}
.sk-row:last-child{ border-bottom:0; }
.sk-row-lines{ flex:1; min-width:0; }
.sk-row-lines .sk-line:last-child{ margin-bottom:0; }

/* A card-shaped placeholder, for the tile grids. */
.sk-card{
  border:1px solid var(--line);
  border-radius:var(--r-md);
  padding:var(--sp-4);
}

/* ---- spinner ----
   For actions, not for pages. A page gets a skeleton; a button that is
   working gets this. */
.spinner{
  display:inline-block;
  width:16px; height:16px;
  border:2px solid color-mix(in srgb, currentColor 25%, transparent);
  border-top-color:currentColor;
  border-radius:50%;
  animation:spin .6s linear infinite;
  flex:none;
}
.spinner.lg{ width:26px; height:26px; border-width:2.5px; }
@keyframes spin{ to{ transform:rotate(360deg); } }

.spinner-block{
  display:flex;
  flex-direction:column;
  align-items:center;
  justify-content:center;
  gap:var(--sp-3);
  padding:var(--sp-8) var(--sp-4);
  color:var(--tx3);
  font-size:13px;
}

/* A button that is mid-request keeps its width — swapping the label for
   a spinner would make the button, and everything beside it, jump. */
.btn.is-busy, .icon-btn.is-busy{
  pointer-events:none;
  position:relative;
  color:transparent !important;
}
.btn.is-busy > *, .icon-btn.is-busy > *{ opacity:0; }
.btn.is-busy::after, .icon-btn.is-busy::after{
  content:"";
  position:absolute;
  top:50%; left:50%;
  width:16px; height:16px;
  margin:-8px 0 0 -8px;
  border:2px solid color-mix(in srgb, var(--btn-tx) 30%, transparent);
  border-top-color:var(--btn-tx);
  border-radius:50%;
  animation:spin .6s linear infinite;
}
.btn.ghost.is-busy::after, .icon-btn.is-busy::after{
  border-color:color-mix(in srgb, var(--tx2) 30%, transparent);
  border-top-color:var(--tx2);
}


/* ==================================================================
   7. PRIMITIVES — empty states
   ==================================================================
   The old `.empty` was one line of grey text with no vertical room, no
   icon and nothing to do next. It stays (25+ call sites still use it)
   but gets enough space to read as deliberate; `.empty-state` is the
   real component for a screen with nothing on it. */
.empty{
  color:var(--tx3);
  font-size:13.5px;
  padding:var(--sp-6) var(--sp-4);
  text-align:center;
}

.empty-state{
  display:flex;
  flex-direction:column;
  align-items:center;
  justify-content:center;
  text-align:center;
  gap:var(--sp-3);
  padding:var(--sp-8) var(--sp-5);
  min-height:220px;
}
.empty-state .es-ic{
  width:56px; height:56px;
  border-radius:var(--r-lg);
  display:grid;
  place-items:center;
  background:var(--card2);
  color:var(--tx3);
  margin-bottom:var(--sp-1);
}
.empty-state .es-ic svg{
  width:26px; height:26px;
  fill:none; stroke:currentColor;
  stroke-width:1.7; stroke-linecap:round; stroke-linejoin:round;
}
.empty-state h4{
  margin:0;
  font-size:15.5px;
  font-weight:600;
  color:var(--tx);
}
.empty-state p{
  margin:0;
  font-size:13.5px;
  line-height:1.55;
  color:var(--tx3);
  max-width:34ch;
}
.empty-state .btn{ margin-top:var(--sp-2); }

/* The error flavour: same shape, so a failed load and an empty result
   never reflow the page differently. */
.empty-state.is-error .es-ic{
  background:color-mix(in srgb, var(--bad) 14%, transparent);
  color:var(--bad);
}


/* ==================================================================
   8. PRIMITIVES — inline form validation
   ==================================================================
   Every one of the 43 modals reported failure through a 3-second toast
   at the bottom of the screen, which never said which field was wrong.
   These make the field itself say so. */
.field{
  display:flex;
  flex-direction:column;
  gap:var(--sp-1);
  min-width:0;
}
/* A label tied to its input, so tapping the label focuses the field —
   free on desktop, worth real millimetres of thumb travel on a phone. */
.field > label{
  font-size:12.5px;
  font-weight:500;
  color:var(--tx2);
  margin:0;
}
.field-error{
  display:flex;
  align-items:center;
  gap:var(--sp-1);
  color:var(--bad);
  font-size:12.5px;
  line-height:1.4;
  margin-top:2px;
}
.field-error svg{
  width:14px; height:14px; flex:none;
  fill:none; stroke:currentColor; stroke-width:2;
  stroke-linecap:round; stroke-linejoin:round;
}
input.invalid, select.invalid, textarea.invalid{
  border-color:var(--bad);
}
input.invalid:focus, select.invalid:focus, textarea.invalid:focus{
  border-color:var(--bad);
  box-shadow:0 0 0 3px color-mix(in srgb, var(--bad) 18%, transparent);
}
/* A field that has just been rejected shakes once. It is the fastest way
   to say "here" without stealing focus or opening anything. */
@keyframes nudge{
  0%,100%{ transform:translateX(0); }
  25%{ transform:translateX(-4px); }
  75%{ transform:translateX(4px); }
}
.field.just-failed input,
.field.just-failed select,
.field.just-failed textarea{ animation:nudge .22s var(--ease-out); }

/* Focus ring: visible for keyboard users, absent for a mouse or a
   thumb, which is what :focus-visible is for. */
input:focus-visible, select:focus-visible, textarea:focus-visible,
.btn:focus-visible, .icon-btn:focus-visible, .tab:focus-visible,
.chip:focus-visible, .nav:focus-visible{
  outline:2px solid var(--orange);
  outline-offset:2px;
}


/* ==================================================================
   9. PRIMITIVES — bottom sheet
   ==================================================================
   The native answer to "a modal on a phone". It comes up from the
   thumb, it has a grabber, and it can be thrown back down. ui.ts
   handles the drag; this is its skin. */
.sheet-scrim{
  position:fixed;
  inset:0;
  z-index:var(--z-modal);
  background:rgba(0,0,0,.5);
  display:flex;
  align-items:flex-end;
  justify-content:center;
  animation:fade-in var(--t-base) var(--ease-out);
}
.sheet{
  width:100%;
  max-width:560px;
  max-height:min(88dvh, 88vh);
  display:flex;
  flex-direction:column;
  background:var(--card);
  border:1px solid var(--line);
  border-bottom:0;
  border-radius:var(--r-xl) var(--r-xl) 0 0;
  box-shadow:var(--shadow);
  padding-bottom:var(--safe-b);
  animation:sheet-up var(--t-slow) var(--ease-out);
  /* The drag handler writes translateY here; keeping it off the
     animation's property list is what stops the two fighting. */
  will-change:transform;
}
@keyframes sheet-up{ from{ transform:translateY(100%); } to{ transform:translateY(0); } }
.sheet.is-closing{ animation:sheet-down var(--t-base) var(--ease-out) forwards; }
@keyframes sheet-down{ to{ transform:translateY(100%); } }

.sheet-grab{
  flex:none;
  padding:var(--sp-3) 0 var(--sp-2);
  display:grid;
  place-items:center;
  cursor:grab;
  touch-action:none;
}
.sheet-grab::before{
  content:"";
  width:38px; height:4px;
  border-radius:var(--r-full);
  background:var(--line-strong);
}
.sheet-head{
  flex:none;
  padding:0 var(--sp-5) var(--sp-3);
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:var(--sp-3);
}
.sheet-head h3{ margin:0; font-size:17px; font-weight:600; }
.sheet-body{
  flex:1;
  min-height:0;
  overflow:auto;
  padding:0 var(--sp-5) var(--sp-4);
  display:flex;
  flex-direction:column;
  gap:var(--sp-3);
}
/* Actions stay reachable while the body scrolls — on a tall sheet the
   confirm button must never be the thing you have to scroll to find. */
.sheet-actions{
  flex:none;
  display:flex;
  gap:var(--sp-2);
  padding:var(--sp-3) var(--sp-5) var(--sp-4);
  border-top:1px solid var(--line);
  background:var(--card);
}
.sheet-actions .btn{ flex:1; }


/* ==================================================================
   10. TOAST
   ==================================================================
   One line, above the tab bar, out of the way of the thumb. */
.toast{
  position:fixed;
  left:50%;
  transform:translateX(-50%);
  max-width:min(92vw, 460px);
  display:flex;
  align-items:center;
  gap:var(--sp-2);
  z-index:var(--z-toast);
  border-radius:var(--r-md);
  padding:var(--sp-3) var(--sp-4);
  font-size:13.5px;
  line-height:1.45;
  animation:toast-in var(--t-base) var(--ease-spring);
}
@keyframes toast-in{
  from{ opacity:0; transform:translate(-50%, 14px) scale(.96); }
  to{ opacity:1; transform:translate(-50%, 0) scale(1); }
}


/* ==================================================================
   11. KEYBOARD
   ==================================================================
   When the software keyboard opens, the layout viewport does not change
   — only the visual one does. Anything pinned to the bottom therefore
   ends up underneath the keyboard. ui.ts publishes the keyboard's
   height as --kb; these lift out of its way. */
html[data-vm="phone"] body.kb-open .tabbar{
  /* The bar is navigation, not input. While someone is typing it is in
     the way, so it leaves rather than stacking on the keyboard. */
  transform:translateY(100%);
  transition:transform var(--t-base) var(--ease-out);
  pointer-events:none;
}
html[data-vm="phone"] body.kb-open .toast{
  bottom:calc(var(--kb) + var(--sp-3));
}


/* ==================================================================
   12. FORM GROUPS
   ==================================================================
   A control made of several boxes should read as one control. The old
   markup put a password input and its reveal button side by side as two
   separately-bordered things; a date range was two boxes with four
   borders between them. One border, drawn once, around the group. */
.input-group{
  display:flex;
  align-items:stretch;
  background:var(--card);
  border:1px solid var(--line);
  border-radius:var(--r-md);
  overflow:hidden;
  min-width:0;
}
.input-group:focus-within{ border-color:var(--orange); }
.input-group.invalid,
.input-group:has(.invalid){ border-color:var(--bad); }

/* The children give up their own chrome — the group is wearing it now. */
.input-group > input,
.input-group > select,
.input-group > .btn,
.input-group > .reveal{
  border:0;
  border-radius:0;
  background:transparent;
  min-width:0;
}
.input-group > input{ flex:1; }
.input-group > input:focus{ border:0; box-shadow:none; }
/* A hairline between segments, never a full border: two adjacent borders
   read as a 2px seam and make a single control look like two. Specificity
   matched to the `border:0` reset above, which is a class + a class and
   would otherwise win and erase the seam. */
.input-group > * + *:not(:first-child){ border-left:1px solid var(--line); }

/* The group is one control, so it takes the focus ring as one control.
   A ring on the child is drawn inside the group's overflow:hidden and
   comes out as a bright bar down the seam instead of a ring. */
.input-group > input:focus-visible,
.input-group > select:focus-visible,
.input-group > .reveal:focus-visible{ outline:none; }
.input-group:focus-within{
  outline:2px solid var(--orange);
  outline-offset:2px;
}
/* ...but only when a keyboard is driving. :focus-within has no
   -visible form, so the state is read off the child that has focus. */
.input-group:focus-within:not(:has(:focus-visible)){ outline:none; }

.reveal{
  flex:none;
  width:var(--tap);
  display:grid;
  place-items:center;
  color:var(--tx3);
  cursor:pointer;
  font-family:inherit;
  -webkit-tap-highlight-color:transparent;
}
.reveal svg{
  width:19px; height:19px;
  fill:none; stroke:currentColor; stroke-width:1.8;
  stroke-linecap:round; stroke-linejoin:round;
}
.reveal:active{ color:var(--tx); transform:scale(.9); }

/* Hints carry a verdict, so they need to be able to show one. */
.hint-ok{ color:var(--ok); }
.hint-bad{ color:var(--bad); }

/* Form-level failure: what no single field owns — a locked account, an
   expired one, a server that is not answering. */
.form-err{
  display:flex;
  align-items:flex-start;
  gap:var(--sp-2);
  background:color-mix(in srgb, var(--bad) 12%, transparent);
  border:1px solid color-mix(in srgb, var(--bad) 40%, transparent);
  color:var(--bad);
  border-radius:var(--r-md);
  padding:var(--sp-3);
  font-size:13px;
  line-height:1.45;
  text-align:left;
}


/* ==================================================================
   13. AUTH SCREENS
   ==================================================================
   Login, invite and the forced password change. Three small forms, and
   the first thing anybody sees. */

/* The card was centred inside a fixed, full-height box. That is correct
   until the keyboard opens: the visual viewport halves, the card stays
   centred in the layout viewport, and the field being typed into ends up
   behind the keyboard with no way to scroll to it. Scrolling the wrapper
   and centring only when there is room to fixes both cases. */
.login-wrap{
  display:flex;
  align-items:center;
  justify-content:center;
  overflow-y:auto;
  padding:var(--sp-5) var(--sp-4);
  padding-top:calc(var(--sp-5) + var(--safe-t));
  padding-bottom:calc(var(--sp-5) + var(--safe-b));
}
.login-card{
  /* A card taller than the space left over must be able to scroll past
     the top, which `align-items:center` alone will not allow. */
  margin:auto;
  text-align:left;
}
.login-card .login-logo,
.login-card h1,
.login-card .login-sub{ align-self:center; text-align:center; }
.login-card .field{ width:100%; }
.login-card .hint{ margin:2px 0 0; font-size:12px; line-height:1.45; }

/* No align-items override here on purpose: `margin:auto` on the card
   already both centres it and lets it overflow in BOTH directions when it
   is taller than the space, which `align-items:center` alone famously does
   not — it makes the top of an over-tall flex item unreachable. Setting
   align-items:flex-start as well would only have been silently ignored. */
html[data-vm="phone"] .login-card{
  width:100%;
  max-width:420px;
  padding:var(--sp-6) var(--sp-5) var(--sp-7);
  border-radius:var(--r-xl);
  gap:var(--sp-4);
}
html[data-vm="phone"] .login-card h1{ font-size:24px; }
html[data-vm="phone"] .login-card .login-logo{ width:60px; height:60px; font-size:28px; }
/* Full-width and last, which is where a thumb expects the one thing that
   submits a form on a phone. */
html[data-vm="phone"] .login-card .btn.wide{
  margin-top:var(--sp-2);
  min-height:var(--tap);
  font-size:16px;
}

/* The forced password change is a modal on desktop and a sheet on a
   phone, because it is the very first thing a new teammate meets and it
   should not look like a web dialog. */
html[data-vm="phone"] #pwScrim{
  align-items:flex-end;
  padding:0;
}
html[data-vm="phone"] #pwForm{
  width:100%;
  max-width:100%;
  border-radius:var(--r-xl) var(--r-xl) 0 0;
  padding:var(--sp-6) var(--sp-5);
  padding-bottom:calc(var(--sp-6) + var(--safe-b));
  max-height:92dvh;
  gap:var(--sp-4);
}
html[data-vm="phone"] #pwForm .modal-actions{
  flex-direction:column-reverse;
  gap:var(--sp-2);
}
html[data-vm="phone"] #pwForm .modal-actions .btn{ width:100%; }


/* ==================================================================
   14. APP SHELL — phone
   ==================================================================
   The topbar was carrying six things on a 393px screen: a hamburger, the
   title, three scope switches, a device-preview switcher, a search box,
   a bell and a primary button. It wrapped onto two rows, the search icon
   sat on top of its own placeholder, and "New task" broke across two
   lines. The fix is not tighter spacing — it is fewer things.

     - the hamburger goes: the More tab opens the same drawer;
     - the device switcher goes: it previews a phone, on a phone;
     - "New task" goes to a floating action button above the tab bar,
       which is where a phone's primary action lives;
     - search collapses behind an icon and expands to its own row.

   What is left is one 56px row: title, scope, search, bell. */

.phone-only{ display:none; }
html[data-vm="phone"] .phone-only{ display:inline-grid; }

html[data-vm="phone"] .topbar{
  /* One row. flex-wrap was what allowed the second row to exist at all. */
  flex-wrap:nowrap;
  gap:var(--sp-2);
  min-height:56px;
  padding:var(--sp-2) var(--sp-3);
  padding-top:calc(var(--sp-2) + var(--safe-t));
}
/* Desktop/tablet furniture. The drawer is reachable from More. */
html[data-vm="phone"] .nav-toggle{ display:none; }

html[data-vm="phone"] .title-wrap{ flex:1; min-width:0; }
html[data-vm="phone"] .topbar h1{
  font-size:20px;
  font-weight:650;
  letter-spacing:-.3px;
  min-width:0;
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
}
html[data-vm="phone"] .top-actions{
  /* It was width:100%;order:3 — a full second row of its own. */
  width:auto;
  order:0;
  flex:none;
  gap:var(--sp-1);
}

/* ---- the scope chip ----
   Three separate switches do not fit. Only the company matters at a
   glance; project and team are set from the page that uses them. */
html[data-vm="phone"] #prSwitch,
html[data-vm="phone"] #tmSwitch{ display:none; }
html[data-vm="phone"] .co-switch{
  min-height:36px;
  max-width:112px;
  padding:0 var(--sp-2);
  border-radius:var(--r-full);
  font-size:12.5px;
}
html[data-vm="phone"] .co-switch span{ max-width:64px; }

/* ---- search ----
   Collapsed to an icon; tapping it gives the box the whole bar, with a
   cancel that puts the title back. */
html[data-vm="phone"] .search-wrap{ display:none; }
html[data-vm="phone"] .topbar[data-search="open"] .search-wrap{
  display:flex;
  flex:1;
  min-width:0;
}
html[data-vm="phone"] .topbar[data-search="open"] .title-wrap,
html[data-vm="phone"] .topbar[data-search="open"] #searchToggle,
html[data-vm="phone"] .topbar[data-search="open"] .co-switch,
html[data-vm="phone"] .topbar[data-search="open"] .bell-wrap{ display:none; }

.search-wrap input{ min-width:0; }
.search-cancel{ display:none; }
html[data-vm="phone"] .topbar[data-search="open"] .search-cancel{
  display:grid;
  position:absolute;
  right:2px;
  top:50%;
  transform:translateY(-50%);
  color:var(--tx3);
}
/* Section 4 sets `padding` on every phone input, and a shorthand beats the
   `padding-left:32px` that reserves room for the magnifier — which is how
   the icon ended up drawn on top of its own placeholder. Restated here
   rather than weakened there: every other input does want the shorthand. */
html[data-vm="phone"] .search-wrap input{
  padding-left:36px;
  padding-right:44px;
  width:100%;
}
html[data-vm="phone"] .search-wrap .search-ic{ left:12px; width:17px; height:17px; }


/* ==================================================================
   15. FLOATING ACTION BUTTON
   ==================================================================
   A phone's primary action sits within thumb reach at the bottom, not in
   the top bar. Built by ui.ts from #newTaskBtn so the permission that
   hides one hides the other. */
.fab{ display:none; }

html[data-vm="phone"] .fab{
  display:grid;
  place-items:center;
  position:fixed;
  right:var(--sp-4);
  bottom:calc(var(--tabbar-total) + var(--sp-4));
  width:56px;
  height:56px;
  z-index:calc(var(--z-tabbar) + 1);
  border:0;
  border-radius:var(--r-lg);
  background:var(--orange);
  /* NOT --btn-tx: that is #FFFFFF on the light theme, which measures 2.80 on
     this orange and fails even the 3.0 bar an icon has to clear. */
  color:var(--on-orange);
  box-shadow:0 8px 22px color-mix(in srgb, var(--orange) 36%, transparent),
             0 2px 6px rgba(0,0,0,.3);
  cursor:pointer;
  transition:transform var(--t-fast) var(--ease-out),
             opacity var(--t-base) var(--ease-out);
}
html[data-vm="phone"] .fab.hidden{ display:none; }

/* The + means "new task", and it is only the primary action on the pages
   where tasks are the work. Everywhere else it is a button in the way: in a
   conversation it lands on top of Send — 34% of that button, measured on an
   iPhone 14 Pro, and it is the one in front — so people trying to send a
   message hit "new task" instead.
   ui.ts publishes the open page on the body (syncTabs). An allowlist rather
   than a list of exclusions: a page added later should have to ask for the
   global action, not inherit it and land on top of its own controls. */
html[data-vm="phone"] body:not([data-view="tasks"]):not([data-view="overview"]):not([data-view="boards"]) .fab{
  display:none;
}
.fab svg{
  width:26px; height:26px;
  fill:none; stroke:currentColor; stroke-width:2.2;
  stroke-linecap:round; stroke-linejoin:round;
}
.fab:active{ transform:scale(.92); }
/* While the keyboard is up the tab bar has left, and a button floating in
   front of what is being typed is in the way. */
html[data-vm="phone"] body.kb-open .fab{ opacity:0; pointer-events:none; }
/* A full-screen surface owns the screen; the FAB belongs to the page under it. */
html[data-vm="phone"] body.nav-open .fab,
html[data-vm="phone"] body.overlay-open .fab{ opacity:0; pointer-events:none; }

/* The primary button keeps its place on the bar for tablet and desktop;
   on a phone the FAB is the same action and two of them is one too many. */
html[data-vm="phone"] #newTaskBtn{ display:none; }


/* ==================================================================
   16. OVERVIEW — phone
   ==================================================================
   The page was eight KPI tiles at desktop proportions stacked two-wide,
   which on a 393px screen is four full screens of scrolling before the
   first chart. A tile holds one number and one word; at that job it does
   not need 14px of padding and a 25px number. */

html[data-vm="phone"] .kpis{
  grid-template-columns:repeat(2, minmax(0, 1fr));
  gap:var(--sp-2);
}
html[data-vm="phone"] .kpi{
  padding:var(--sp-3);
  border-radius:var(--r-md);
  /* A consistent height keeps the grid on a baseline whether the number is
     "0" or "1,284" — a ragged grid is most of why this page felt unfinished. */
  min-height:74px;
  display:flex;
  flex-direction:column;
  justify-content:center;
  gap:2px;
}
html[data-vm="phone"] .kpi .n{ font-size:24px; line-height:1.1; }
html[data-vm="phone"] .kpi .l{
  font-size:10px;
  letter-spacing:.5px;
  margin-top:0;
  line-height:1.3;
  /* styles.css gives this --tx3, which measures 2.97 (dark) and 2.92 (light)
     against the tile — under AA for 10px uppercase text by a wide margin.
     Only the phone rule is changed here because that is this pass's remit;
     the same label fails on the desktop layout and on every other --tx3
     caption in the app, which is a token-level decision for its own change. */
  color:var(--tx2);
}

/* ---- hero ----
   Two columns side by side is a desktop shape. On a phone the greeting
   reads first and its actions sit under it, full width. */
html[data-vm="phone"] .ov-hero{
  flex-direction:column;
  align-items:stretch;
  gap:var(--sp-4);
  padding:var(--sp-5) var(--sp-4);
}
html[data-vm="phone"] .ov-hero h2{ font-size:22px; }
html[data-vm="phone"] .ov-hero-r{
  display:grid;
  grid-template-columns:1fr 1fr;
  gap:var(--sp-2);
}
/* The FAB is this same action, in the place a thumb reaches for it. Two
   New-task buttons on one screen is one too many. */
html[data-vm="phone"] .ov-newtask{ display:none; }
html[data-vm="phone"] .ov-hero-r .btn{ width:100%; }
html[data-vm="phone"] .ov-chip{ min-height:var(--tap-sm); padding:0 var(--sp-3) 0 var(--sp-1); }

/* A proportion chart's label column was a fixed 130px — half the screen. */
html[data-vm="phone"] .bar-row{
  grid-template-columns:minmax(0, 1fr) 1.4fr 34px;
  gap:var(--sp-2);
  font-size:12px;
}


/* ==================================================================
   17. TASKS — phone
   ==================================================================
   The page opened with nine metric tiles and three breakdown charts —
   about 1400px, four screenfuls — before the first task. Then came seven
   multi-selects, two toggles and a reset, wrapped into a wall taller
   again than the list underneath it.

   A list page should open on its list. The metrics become one compact
   strip that scrolls sideways, the breakdown charts are desktop ambience
   and stay there, and the filters move into a sheet behind one button
   that says how many are on. */

/* ---- the metrics strip ---- */
html[data-vm="phone"] #taskMetrics{
  padding:var(--sp-3);
}
html[data-vm="phone"] .mtiles{
  display:flex;
  gap:var(--sp-2);
  overflow-x:auto;
  /* Each tile lands square against the edge instead of stopping halfway,
     which is the difference between a scroller and a stuck page. */
  scroll-snap-type:x proximity;
  scroll-padding-left:var(--sp-3);
  margin:0 calc(var(--sp-3) * -1);
  padding:0 var(--sp-3) var(--sp-1);
  scrollbar-width:none;
}
html[data-vm="phone"] .mtiles::-webkit-scrollbar{ display:none; }
html[data-vm="phone"] .mtile{
  flex:none;
  min-width:104px;
  scroll-snap-align:start;
  padding:var(--sp-2) var(--sp-3);
  border-radius:var(--r-md);
}
html[data-vm="phone"] .mtile b{ font-size:19px; }
/* Ambient context, not something anybody acts on from a phone. */
html[data-vm="phone"] .mbreak{ display:none; }
html[data-vm="phone"] .mhead{ margin-bottom:var(--sp-2); }

/* ---- the filter bar ---- */
html[data-vm="phone"] .tk-mobile-bar{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:var(--sp-3);
}
html[data-vm="phone"] .tk-filter-btn{
  gap:var(--sp-2);
  padding:0 var(--sp-4);
}
.tk-filter-btn.on{ border-color:var(--orange); color:var(--orange-2); }
.tk-filter-count{
  min-width:20px;
  height:20px;
  padding:0 5px;
  border-radius:var(--r-full);
  background:var(--orange);
  color:var(--on-orange);
  font-size:11px;
  font-weight:700;
  font-style:normal;
  line-height:20px;
  text-align:center;
}
/* The real controls live in the page and are lent to the sheet, so both
   places have to lay them out. In the page: hidden on a phone.
   Keyed on the class, not the id: the Archive has a filter row of its own and
   borrows the same sheet, and a second copy of these eleven rules is how the
   two would drift. */
html[data-vm="phone"] .tk-filter-row,
html[data-vm="phone"] .tk-row2{ display:none; }
/* ...and in the sheet: one control per line, full width, nothing wrapped. */
html[data-vm="phone"] .tk-filter-row.in-sheet{
  display:flex;
  flex-direction:column;
  align-items:stretch;
  gap:var(--sp-3);
}
html[data-vm="phone"] .in-sheet .ms,
html[data-vm="phone"] .in-sheet .ms-btn{ width:100%; max-width:none; }
html[data-vm="phone"] .in-sheet .ms-btn{ justify-content:space-between; }
/* Inside a sheet the pop-up has a sheet to sit in, so it stops being
   fixed to the screen and becomes part of the flow. */
html[data-vm="phone"] .in-sheet .ms-pop{
  position:static;
  inset:auto;
  max-height:46vh;
  margin-top:var(--sp-2);
  box-shadow:none;
}
html[data-vm="phone"] .in-sheet .toggle{
  justify-content:space-between;
  border:1px solid var(--line);
  border-radius:var(--r-md);
  padding:0 var(--sp-3);
  min-height:var(--tap);
  /* Label first, control last — the phone convention for a settings row. */
  flex-direction:row-reverse;
}
html[data-vm="phone"] .in-sheet .filter-reset{ width:100%; }

/* The chips naming what is currently narrowing the list stay in the page:
   they are the answer to "why am I seeing these", which is a question you
   have while looking at the list, not while in the filter sheet. */
html[data-vm="phone"] .active-filters{
  display:flex;
  gap:var(--sp-2);
  overflow-x:auto;
  flex-wrap:nowrap;
  scrollbar-width:none;
  margin-top:var(--sp-3);
}
html[data-vm="phone"] .active-filters::-webkit-scrollbar{ display:none; }
html[data-vm="phone"] .active-filters > *{ flex:none; }


/* ==================================================================
   18. TASK DRAWER — phone
   ==================================================================
   Full-screen already, but laid out for 780px: a 150px label column ate
   40% of a 393px screen and left the values crammed against the edge,
   and the action row's four buttons each broke across two or three
   lines because they were sharing one row that could never fit them. */

html[data-vm="phone"] .drawer{
  /* It covers the tab bar, which is right — this IS the screen now. */
  padding-top:var(--safe-t);
  padding-bottom:var(--safe-b);
}
html[data-vm="phone"] .drawer-head{
  padding:var(--sp-3) var(--sp-4) var(--sp-2);
  align-items:flex-start;
  gap:var(--sp-2);
}
html[data-vm="phone"] .drawer-head h2{ font-size:18px; line-height:1.3; }
html[data-vm="phone"] .drawer-body{
  padding:var(--sp-4) var(--sp-4) calc(var(--sp-8) + var(--safe-b));
}

/* ---- the eight tabs ----
   They scroll sideways, and nothing said so. A snap stop per tab plus a
   fade at the trailing edge is how a phone admits there is more. */
html[data-vm="phone"] .drawer-tabs{
  padding:var(--sp-1) var(--sp-4) 0;
  gap:var(--sp-1);
  scroll-snap-type:x proximity;
  scrollbar-width:none;
  -webkit-mask-image:linear-gradient(to right, #000 88%, transparent);
          mask-image:linear-gradient(to right, #000 88%, transparent);
}
html[data-vm="phone"] .drawer-tabs::-webkit-scrollbar{ display:none; }
html[data-vm="phone"] .dtab{ scroll-snap-align:start; }

/* ---- the detail list ----
   `150px 1fr` is a desktop definition list. On a phone the label goes
   above its value, both full width — which is also what stops a long
   value being squeezed into 198px and wrapping five times. */
html[data-vm="phone"] .kv,
html[data-vm="phone"] .edit-grid{
  display:flex;
  flex-direction:column;
  gap:0;
}
html[data-vm="phone"] .kv dt,
html[data-vm="phone"] .edit-grid > label{
  font-size:11px;
  text-transform:uppercase;
  letter-spacing:.5px;
  color:var(--tx3);
  margin:0;
  padding-top:var(--sp-3);
}
html[data-vm="phone"] .kv dd{
  margin:var(--sp-1) 0 0;
  font-size:14.5px;
  /* A row per fact, separated rather than crowded — the only thing that
     made the desktop two-column version readable was the column itself. */
  padding-bottom:var(--sp-3);
  border-bottom:1px solid var(--line);
}
html[data-vm="phone"] .kv dd:last-child{ border-bottom:0; }
html[data-vm="phone"] .edit-grid > :not(label){
  margin:var(--sp-1) 0 var(--sp-2);
  width:100%;
}

/* ---- the action row ----
   Four buttons in one wrapping flex row is how "Ask blocking question"
   became three lines tall. Two per row, equal width, no wrap inside. */
html[data-vm="phone"] .task-acts{
  display:grid;
  grid-template-columns:1fr 1fr;
  gap:var(--sp-2);
  margin:var(--sp-4) 0 var(--sp-2);
}
html[data-vm="phone"] .task-acts .btn{
  width:100%;
  min-height:var(--tap);
  padding:0 var(--sp-2);
  font-size:13.5px;
  line-height:1.2;
  /* The label may be two words; it must not be two words on two lines
     with the second one clipped. */
  white-space:normal;
  text-align:center;
}
/* The timer is the action people come here for — it gets the full row. */
html[data-vm="phone"] .task-acts #dStartTimer,
html[data-vm="phone"] .task-acts #dStopTimer{ grid-column:1 / -1; }

/* ---- quick-add rows ---- */
.quick-add{ margin-top:var(--sp-2); }
.quick-add > .btn{
  flex:none;
  border-radius:0;
  min-height:0;
  align-self:stretch;
  padding:0 var(--sp-4);
}
html[data-vm="phone"] .quick-add > input{ min-height:var(--tap); }

/* styles.css pins `.modal-actions` to the bottom of its scroll container on a
   phone, which is right inside a modal — the confirm button must not be the
   thing you have to scroll to find. But the drawer reuses that same class for
   its in-page action rows, and there it meant the buttons floated over the
   detail list: measured, .kv spanned 147..1050 while the action row sat at
   698..812, painted on top of four of its rows. Sticky belongs to a modal. */
html[data-vm="phone"] .drawer-body .modal-actions{
  position:static;
  bottom:auto;
  background:none;
  padding-top:0;
}


/* ==================================================================
   19. CHATS — phone
   ==================================================================
   Two panes, one at a time, which styles.css already does. What it did
   not do was let the open conversation own the screen: it stayed inside
   a card, inside the view's padding, so a message thread had a 12px
   margin, rounded corners and a border on a device where the thread IS
   the screen. */

/* The conversation, full-bleed. The list keeps its padding — it is a
   list of rows, and rows want a margin. */
html[data-vm="phone"] .chat-layout[data-pane="open"]{
  margin:calc(var(--sp-3) * -1) calc(var(--sp-3) * -1) 0;
  width:calc(100% + var(--sp-3) * 2);
}
html[data-vm="phone"] .chat-layout[data-pane="open"] .chat-pane{
  border-radius:0;
  border-left:0;
  border-right:0;
}
html[data-vm="phone"] .chat-head{
  padding:var(--sp-3) var(--sp-3);
  gap:var(--sp-2);
  min-height:52px;
}
html[data-vm="phone"] .chat-msgs{ padding:var(--sp-3); }

/* ---- the composer ----
   It is the bottom of the screen while a conversation is open, so it
   carries the home-indicator inset — and gives it up the moment the
   keyboard is there instead, because then the keyboard is the bottom. */
html[data-vm="phone"] .chat-compose{
  padding:var(--sp-2) var(--sp-3);
  padding-bottom:calc(var(--sp-2) + var(--safe-b));
  gap:var(--sp-1);
  align-items:flex-end;
}
html[data-vm="phone"] body.kb-open .chat-compose{
  padding-bottom:var(--sp-2);
}
html[data-vm="phone"] .chat-compose textarea{
  /* 16px so iOS does not zoom, and no min-height: a composer that starts
     48px tall on a one-line message is a composer that ate the thread. */
  min-height:40px;
  font-size:16px;
  padding:var(--sp-2) var(--sp-3);
  border-radius:var(--r-lg);
}
html[data-vm="phone"] .chat-compose .icon-btn{ min-width:40px; min-height:40px; }
html[data-vm="phone"] #sendBtn{
  min-height:40px;
  padding:0 var(--sp-3);
  border-radius:var(--r-lg);
}

/* The back arrow is the only way out of a conversation on a phone, so it
   is a proper target rather than an icon squeezed beside the title. */
html[data-vm="phone"] .chat-back{ min-width:40px; min-height:40px; margin-left:-6px; }

/* A conversation covers the screen; the tab bar under it is navigation
   away from a thread somebody is reading, and the FAB creates tasks, not
   messages. Both stay — the thread scrolls above them — but the composer
   must clear the bar. */
html[data-vm="phone"] .chat-layout[data-pane="open"] .chat-pane{
  margin-bottom:0;
}

/* The channel list's rows are the primary target on this page. */
html[data-vm="phone"] .chan{
  min-height:var(--tap);
  padding:var(--sp-2) var(--sp-3);
  border-radius:var(--r-md);
  justify-content:center;
}
html[data-vm="phone"] .chan-find{ font-size:16px; }
html[data-vm="phone"] .chat-side-head h3{ font-size:15px; }


/* ==================================================================
   20. DATA TABLES — phone
   ==================================================================
   Tasks and Archive already become cards (styles.css does that, using the
   data-l labels their renderers emit). The other five — Time card,
   Performance, Documents, Database, Admin — are genuine grids that have
   to stay grids, so the work here is making a sideways scroll something
   you can tell is there and can navigate.

   Measured before this: the Database grid is 4466px wide on a 393px
   screen, Admin 959px, Performance 680px. None of them said so. */
html[data-vm="phone"] .table-wrap,
html[data-vm="phone"] #pfBoardWrap{
  overflow-x:auto;
  -webkit-overflow-scrolling:touch;
  overscroll-behavior-x:contain;
  scrollbar-width:none;
  /* The fade only appears while there is something still to the right of
     it, which is what makes it an affordance rather than decoration. */
  -webkit-mask-image:linear-gradient(to right, #000 92%, transparent);
          mask-image:linear-gradient(to right, #000 92%, transparent);
}
html[data-vm="phone"] .table-wrap::-webkit-scrollbar{ display:none; }

/* The first column is what tells you which row you are reading; scrolled
   out of sight, every other column becomes anonymous numbers. */
html[data-vm="phone"] #dbGrid th:first-child,
html[data-vm="phone"] #dbGrid td:first-child,
html[data-vm="phone"] #perfTask th:first-child,
html[data-vm="phone"] #perfTask td:first-child{
  position:sticky;
  left:0;
  z-index:2;
  background:var(--card);
  /* A hairline, so the frozen column reads as an edge rather than as a
     cell that happens to be on top. */
  box-shadow:1px 0 0 var(--line);
}
html[data-vm="phone"] #dbGrid thead th:first-child,
html[data-vm="phone"] #perfTask thead th:first-child{ z-index:3; }


/* ==================================================================
   21. PERFORMANCE, BOARDS, CALENDAR, ADMIN — phone
   ================================================================== */

/* ---- the People/Teams/Roles/Managers segmented control ----
   Measured at 31px, the smallest real target left on any page. */
html[data-vm="phone"] .seg{ width:100%; }
html[data-vm="phone"] .seg button{
  min-height:var(--tap-min);
  font-size:12.5px;
  padding:0 var(--sp-1);
}
html[data-vm="phone"] .perf-bar{
  display:grid;
  grid-template-columns:1fr 1fr;
  gap:var(--sp-2);
  align-items:center;
}
html[data-vm="phone"] .perf-bar select{ width:100%; }
html[data-vm="phone"] .perf-bar b{ grid-column:1 / -1; text-align:center; }
html[data-vm="phone"] #pfCustom{
  grid-column:1 / -1;
  display:flex;
  gap:var(--sp-2);
}
html[data-vm="phone"] #pfCustom input{ flex:1; min-width:0; }

/* ---- boards ----
   A kanban is a sideways scroll by nature; it just has to stop halfway
   between two columns. */
html[data-vm="phone"] .kanban{
  scroll-snap-type:x mandatory;
  scroll-padding-left:var(--sp-3);
  gap:var(--sp-3);
  scrollbar-width:none;
}
html[data-vm="phone"] .kanban::-webkit-scrollbar{ display:none; }
html[data-vm="phone"] .kcol{
  scroll-snap-align:start;
  /* Near-full width: one column at a time is how a phone reads a board,
     with just enough of the next showing to say it is there. */
  min-width:clamp(240px, 84vw, 320px);
  width:clamp(240px, 84vw, 320px);
}
html[data-vm="phone"] .kcard{ padding:var(--sp-3); border-radius:var(--r-md); }

/* ---- calendar ----
   A month grid on a 393px screen gives each day 56px. That is enough for
   a number and a dot, and not enough for the event chips it was drawing
   at 9px — which is a size nobody reads, taking the space the number
   needed to stay legible. */
html[data-vm="phone"] .cal-bar{
  display:grid;
  grid-template-columns:auto 1fr auto;
  gap:var(--sp-2);
  align-items:center;
}
html[data-vm="phone"] .cal-bar .chips{
  grid-column:1 / -1;
  overflow-x:auto;
  flex-wrap:nowrap;
  scrollbar-width:none;
}
html[data-vm="phone"] .cal-bar .chips::-webkit-scrollbar{ display:none; }
html[data-vm="phone"] .cal-cell{ min-height:62px; padding:var(--sp-1); }
html[data-vm="phone"] .cal-chip{
  font-size:10px;
  padding:0 var(--sp-1);
  min-height:16px;
  line-height:16px;
}

/* ---- admin ----
   Eleven tabs in three groups. They already scroll sideways; they did not
   have a target size or anything saying they scroll. */
html[data-vm="phone"] #adminTabs{
  scroll-snap-type:x proximity;
  scrollbar-width:none;
  -webkit-mask-image:linear-gradient(to right, #000 92%, transparent);
          mask-image:linear-gradient(to right, #000 92%, transparent);
}
html[data-vm="phone"] #adminTabs::-webkit-scrollbar{ display:none; }
html[data-vm="phone"] #adminTabs .chip{ scroll-snap-align:start; }

/* ---- files ---- */
html[data-vm="phone"] .file-grid{
  grid-template-columns:repeat(2, minmax(0, 1fr));
  gap:var(--sp-2);
}

/* ---- activity feed ----
   calc(100dvh - 230px) was measured against a desktop topbar. */
html[data-vm="phone"] .feed.tall{
  max-height:none;
  overflow:visible;
}


/* ==================================================================
   22. EMPTY STATES IN LAYOUTS
   ==================================================================
   An empty state describes a whole container, so it has to occupy the
   whole container. Dropped into a two-column grid it became one cell —
   a centred column of text down the left half of the screen, which is
   how the Files page looked. */
.empty-state{ grid-column:1 / -1; width:100%; }
/* Inside a table it stands in for every column at once. */
td > .empty-state, td .empty-state{ margin-inline:auto; }

/* A toolbar of two controls is a row on a desktop and a stack on a phone;
   side by side, the filter box loses half its placeholder and the button
   beside it loses its label. */
html[data-vm="phone"] .row-between{
  flex-direction:column;
  align-items:stretch;
  gap:var(--sp-2);
}
html[data-vm="phone"] .row-between > input[type=search],
html[data-vm="phone"] .row-between > .btn{ width:100%; max-width:none; }
html[data-vm="phone"] #docsSearch,
html[data-vm="phone"] #filesSearch,
html[data-vm="phone"] #dbTableFind{ width:100%; max-width:none; }


/* ==================================================================
   23. MODALS — phone
   ==================================================================
   styles.css already turns a modal into a bottom sheet here. What it
   does not do is clear the home indicator: measured, the last control
   sat 22px from the bottom of the screen, which on any modern iPhone is
   underneath the bar. */
html[data-vm="phone"] .modal{
  border-radius:var(--r-xl) var(--r-xl) 0 0;
  padding-bottom:calc(var(--sp-5) + var(--safe-b));
  gap:var(--sp-3);
}
html[data-vm="phone"] .modal-actions{
  padding-bottom:var(--safe-b);
  gap:var(--sp-2);
}
/* Stacked, primary last: on a phone the thumb rests at the bottom, and
   the bottom is where the action you meant should be. */
html[data-vm="phone"] .modal-actions{
  flex-direction:column-reverse;
}
html[data-vm="phone"] .modal-actions .btn{ width:100%; }
html[data-vm="phone"] .modal label{
  font-size:12.5px;
  font-weight:500;
  color:var(--tx2);
  margin-bottom:var(--sp-1);
}
/* A form grid of two columns on a 393px screen is two half-width fields. */
html[data-vm="phone"] .modal .form-grid{ grid-template-columns:1fr; }


/* ==================================================================
   24. DONE-MARK TOGGLE (assignee chain)
   ==================================================================
   Was a <span class="mv"> holding one glyph — no focus, no role, a hit area
   the size of the character. It is a real button now, sized as one. */
.done-toggle{
  display:inline-grid;
  place-items:center;
  min-width:24px;
  height:24px;
  margin-left:2px;
  padding:0 6px;
  border:1px solid var(--line);
  border-radius:var(--r-full);
  background:var(--card);
  color:var(--tx2);
  font:inherit;
  font-size:12px;
  line-height:1;
  cursor:pointer;
}
.done-toggle:hover{ border-color:var(--orange); color:var(--tx); }
/* The undo direction is the one with consequences for somebody else, so it
   is the one that looks like it has them. */
.done-toggle.is-done{ color:var(--warn); border-color:color-mix(in srgb, var(--warn) 45%, var(--line)); }
.done-toggle:active{ transform:scale(.92); }
html[data-vm="phone"] .done-toggle{
  position:relative;
  min-width:30px;
  height:30px;
  font-size:14px;
}
/* 30px visible, 44px to the thumb — the chain is a row of pills and a 44px
   control would break it apart. */
html[data-vm="phone"] .done-toggle::after{
  content:"";
  position:absolute;
  top:50%; left:50%;
  width:var(--tap-min); height:var(--tap-min);
  transform:translate(-50%,-50%);
}


/* ==================================================================
   25. UNREAD BADGES — and the one that is for you
   ==================================================================
   Every unread badge carries its count, Telegram-style. A conversation
   where one of the unread messages @mentions you gets a different badge:
   red, pulsing, and prefixed with @ — so "somebody talked" and "somebody
   talked TO YOU" stop looking identical.

   The @ is not decoration. Red against orange is exactly the pair many
   colour-blind people cannot separate, and anyone who has asked for
   reduced motion gets no pulse (section 3 turns animation off for them).
   The @ is the difference that survives both. */

:root{
  /* White on this red measures 4.63:1; on the dark theme's --bad (#F87171)
     white is 2.5:1. A badge is small bold text, so it needs the solid one
     in both themes. */
  --mention-fill:#DC2626;
  --mention-ink:#FFFFFF;
}

/* A transform does nothing to an inline box, and the pulse is a transform. */
.chan .unread{
  display:inline-flex;
  align-items:center;
  justify-content:center;
  min-width:20px;
  height:20px;
  padding:0 6px;
  border-radius:var(--r-full);
  font-variant-numeric:tabular-nums;
  line-height:1;
}

.chan .unread.mention,
.badge.mention,
.tab .tab-badge.mention{
  background:var(--mention-fill);
  color:var(--mention-ink);
  animation:mention-pulse 1.5s var(--ease-out) infinite;
  /* Scaling from the centre, so the count does not drift as it breathes. */
  transform-origin:center;
}
@keyframes mention-pulse{
  0%, 100% { transform:scale(1);    box-shadow:0 0 0 0 color-mix(in srgb, var(--mention-fill) 55%, transparent); }
  45%      { transform:scale(1.16); box-shadow:0 0 0 5px color-mix(in srgb, var(--mention-fill) 0%, transparent); }
}
/* The sidebar badge sits in a row; scaling it must not shove the label. */
.badge.mention{ display:inline-block; }

/* The row itself says so as well — the preview line is what you read. */
.chan.has-mention .cl{ color:var(--tx); }
.chan.has-mention .ct{ font-weight:650; }
.chan .cl-gone{ color:var(--tx3); font-style:italic; }


/* ==================================================================
   26. SUB-TASKS IN THE LIST
   ==================================================================
   A parent and the tasks it groups are one row until asked otherwise.
   Before this the list was flat and ordered by number, so a story's
   eight sub-tasks were eight more rows scattered among unrelated work —
   and since a sub-task usually gets a higher number than the task it was
   created from, children routinely sorted above their own parent. */

.tk-kids{
  display:inline-flex;
  align-items:center;
  gap:3px;
  margin-right:6px;
  padding:0 6px 0 3px;
  height:20px;
  border:1px solid var(--line);
  border-radius:var(--r-full);
  background:var(--card2);
  color:var(--tx2);
  font:inherit;
  font-size:11px;
  line-height:1;
  cursor:pointer;
  vertical-align:middle;
}
.tk-kids b{ font-weight:650; font-variant-numeric:tabular-nums; }
.tk-kids:hover{ border-color:var(--orange); color:var(--tx); }
.tk-kids:active{ transform:scale(.94); }
.tk-caret{
  font-style:normal;
  font-size:9px;
  display:inline-block;
  transition:transform var(--t-fast) var(--ease-out);
}
.tk-kids.open{ border-color:var(--orange); color:var(--orange-2); background:var(--orange-dim); }
.tk-kids.open .tk-caret{ transform:rotate(90deg); }

/* The child rows read as belonging to the row above them: indented, with a
   rule down the side standing in for the line a tree would draw. */
#taskTable tr.tk-sub td.detail{
  padding-left:30px;
  position:relative;
}
#taskTable tr.tk-sub td.detail::before{
  content:"";
  position:absolute;
  left:14px; top:0; bottom:0;
  border-left:2px solid var(--line);
}
#taskTable tr.tk-sub td.detail .td-title{ color:var(--tx2); }
#taskTable tr.tk-parent.open td.detail{ font-weight:600; }

/* On a phone each row is a card, so an indent is a card that starts further
   in, with the same rule down its edge. */
html[data-vm="phone"] #taskTable tr.tk-sub{
  margin-left:var(--sp-5);
  border-left:2px solid var(--line-strong);
  border-top-left-radius:0;
  border-bottom-left-radius:0;
}
html[data-vm="phone"] #taskTable tr.tk-sub td.detail{ padding-left:0; }
html[data-vm="phone"] #taskTable tr.tk-sub td.detail::before{ display:none; }
html[data-vm="phone"] .tk-kids{ height:30px; padding:0 10px 0 7px; font-size:12.5px; position:relative; }
/* 30px visible, 44px to the thumb — same trick as the done toggle. */
html[data-vm="phone"] .tk-kids::after{
  content:"";
  position:absolute;
  top:50%; left:50%;
  width:var(--tap-min); height:var(--tap-min);
  transform:translate(-50%,-50%);
}


/* ==================================================================
   27. RELEASES
   ==================================================================
   One card per version: what ships, when, who is ready, and what it
   cost. Deliberately thin — the tasks themselves are edited on the
   Tasks page, and a second editor here would be a second thing to
   keep in step. */
.rel-card{ display:flex; flex-direction:column; gap:var(--sp-3); }
/* A blocked release should be findable by scrolling, not by reading. */
.rel-card.blocked{ border-color:color-mix(in srgb, var(--bad) 45%, var(--line)); }

.rel-head{
  display:flex;
  align-items:flex-start;
  justify-content:space-between;
  gap:var(--sp-3);
  flex-wrap:wrap;
}
.rel-head > div:first-child{ display:flex; align-items:center; gap:var(--sp-2); flex-wrap:wrap; }
.rel-v{ font-size:17px; letter-spacing:-.01em; }
.rel-proj{
  font-size:12px;
  color:var(--tx2);
  background:var(--card2);
  border:1px solid var(--line);
  border-radius:var(--r-full);
  padding:2px 9px;
}
.rel-when{ font-size:13px; color:var(--tx2); text-align:right; }
.rel-when i{ display:block; font-style:normal; font-size:11.5px; color:var(--tx3); }
.rel-notes{ margin:0; color:var(--tx2); font-size:13px; line-height:1.5; }

.rel-block{
  background:color-mix(in srgb, var(--bad) 10%, transparent);
  border:1px solid color-mix(in srgb, var(--bad) 35%, transparent);
  border-radius:var(--r-md);
  padding:var(--sp-3);
  font-size:13px;
}
.rel-block b{ color:var(--bad); }
.rel-reason{ color:var(--tx2); margin-top:4px; line-height:1.5; }

.rel-voters{ display:flex; flex-wrap:wrap; gap:var(--sp-2); align-items:center; }
.rel-voter{
  display:inline-flex;
  align-items:center;
  gap:5px;
  font-size:12px;
  border-radius:var(--r-full);
  padding:3px 10px;
  border:1px solid var(--line);
  background:var(--card2);
  color:var(--tx2);
}
.rel-voter.ok{ color:var(--ok); border-color:color-mix(in srgb, var(--ok) 40%, var(--line)); }
.rel-voter.no{ color:var(--bad); border-color:color-mix(in srgb, var(--bad) 45%, var(--line)); }

.rel-foot{
  display:flex;
  align-items:center;
  gap:var(--sp-4);
  flex-wrap:wrap;
  border-top:1px solid var(--line);
  padding-top:var(--sp-3);
}
.rel-stat{ font-size:12.5px; color:var(--tx3); }
.rel-stat b{ color:var(--tx); font-size:14px; }
.rel-actions{ display:flex; gap:var(--sp-2); margin-left:auto; flex-wrap:wrap; }

.rel-tasks{ width:100%; font-size:13px; }
.rel-tasks td{ padding:8px 10px; border-bottom:1px solid var(--line); }

/* On a phone the actions are the point of the card, so they get the width. */
html[data-vm="phone"] .rel-head{ flex-direction:column; gap:var(--sp-1); }
html[data-vm="phone"] .rel-when{ text-align:left; }
html[data-vm="phone"] .rel-actions{ margin-left:0; width:100%; }
html[data-vm="phone"] .rel-actions .btn{ flex:1; }
html[data-vm="phone"] .rel-foot{ gap:var(--sp-3); }

/* An objection, and what became of it. A resolved one keeps its place — what
   was objected to and who overruled it is the record — but stops shouting. */
.rel-obj{
  display:flex;
  align-items:flex-start;
  justify-content:space-between;
  gap:var(--sp-3);
  padding:var(--sp-2) 0;
  border-top:1px solid color-mix(in srgb, var(--bad) 20%, transparent);
}
.rel-obj:first-of-type{ border-top:0; }
.rel-obj > div{ min-width:0; }
.rel-obj b{ display:block; font-size:12.5px; }
.rel-obj .rel-reason{ display:block; }
.rel-obj .btn{ flex:none; }
.rel-obj.done b,
.rel-obj.done .rel-reason{ color:var(--tx3); text-decoration:line-through; }
.rel-resolved{
  display:inline-block;
  margin-top:3px;
  font-size:11.5px;
  color:var(--ok);
  text-decoration:none;
}
/* Every objection settled: still on the record, no longer an alarm. */
.rel-block.settled{
  background:none;
  border-color:var(--line);
}
.rel-block.settled b{ color:var(--tx2); }
html[data-vm="phone"] .rel-obj{ flex-direction:column; gap:var(--sp-2); }
html[data-vm="phone"] .rel-obj .btn{ width:100%; }

/* Four actions across a 393px card is four two-line buttons. Two by two, with
   the pair that decides the release on the row a thumb reaches first. */
html[data-vm="phone"] .rel-actions{
  display:grid;
  grid-template-columns:1fr 1fr;
  gap:var(--sp-2);
}
html[data-vm="phone"] .rel-actions .btn{ width:100%; white-space:nowrap; }

/* The Releases page carries its own primary action, and the floating one
   creates a TASK — two orange buttons a thumb apart meaning different things.
   The page's own wins on the page it belongs to. */
html[data-vm="phone"] body[data-view="releases"] .fab{ display:none; }
