/* ==========================================================================
   SFC Workspace — app shell                        Rebuilt at Phase 8.5

   Boot · login · sidebar · top bar · notification panel · toasts · chips ·
   panels · buttons · mobile tab bar.

   PAPER UNDER GLASS (§3.2). Content is opaque and still. Chrome floats above
   it and is translucent. Every glassy surface here declares its opaque
   fallback first and only then asks for the blur, because roughly a fifth of
   users never get `backdrop-filter` and must receive a design rather than a
   pale rectangle.

   No colour literals — every value is a token from tokens.css (rule 7).
   ========================================================================== */

/* ---------- 1. Reset + the surfaces nobody draws -------------------------
   Selection, caret, focus ring, scrollbars and the numerals in data all ship
   with browser defaults belonging to no design system. Theming them is the
   cheapest signal that a page was built rather than assembled. */

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

/* THE `hidden` ATTRIBUTE MUST OUTRANK A COMPONENT'S OWN `display`.
   The UA stylesheet's `[hidden] { display: none }` is a USER-AGENT rule, so
   any author rule setting `display` beats it — and this app has several that
   do. `.check` is `display: inline-flex`, so `el.toggleAttribute('hidden')`
   set the attribute, the DOM inspector showed it set, and the control stayed
   on screen: the attendance checkbox was visible with Person on "Everyone".
   Nothing errors, and the JS reads as correct because it IS correct.

   Sixth cascade trap of this build, and the same shape as the others — a rule
   that appears to apply and does not. `!important` is right here: `hidden`
   means hidden, and a component that wants to override it is asking for the
   bug above. */
[hidden] { display: none !important; }

html, body { margin: 0; padding: 0; }
/* RESERVE THE SCROLLBAR, ALWAYS.
   A view whose content is one line taller than the viewport grows a scrollbar,
   the layout narrows by its width, and everything shifts sideways — then snaps
   back on the next view. It reads as a fast double-click. `stable` keeps the
   gutter there whether or not the bar is showing, so switching views moves
   nothing horizontally. Applied to the document AND to every panel that
   scrolls independently, because each one has the same failure. */
html { -webkit-text-size-adjust: 100%; scrollbar-gutter: stable; }

body {
  font-family: var(--font);
  font-size: var(--fs-500);
  line-height: var(--lh-body);
  background: var(--bg);
  color: var(--text);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  -webkit-tap-highlight-color: transparent;
  accent-color: var(--primary);
  caret-color: var(--primary);
  scrollbar-color: var(--line) transparent;
  scrollbar-width: thin;
}

::selection { background: color-mix(in srgb, var(--primary) 26%, transparent); color: var(--text); }

::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: var(--line);
  border-radius: var(--r-pill);
  border: 2px solid transparent;
  background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover { background: var(--text-3); background-clip: content-box; }

button, input, select, textarea { font: inherit; color: inherit; }
button { cursor: pointer; background: none; border: 0; padding: 0; }
/* Anchors in this app are navigation, never prose links: the nav items are
   real <a href> for middle-click and the address bar, and shipped underlined
   because nothing ever turned the default off. */
a { color: inherit; text-decoration: none; }
svg { width: 16px; height: 16px; flex: none; }

/* Numerals in data line up. Counts, dates, times, sizes. */
.tnum, .nav-item .count, .icon-btn .badge, .chip, time { font-variant-numeric: tabular-nums; }

:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}
input:focus-visible, select:focus-visible, textarea:focus-visible {
  outline-offset: 0;
  box-shadow: 0 0 0 3px var(--focus-glow);
}

/* Browser surfaces, part two: form controls. Without these every <select>
   paints the platform chevron and every <input type="date"> its own spinner,
   in an Arabic-first UI (§3.6). The date VALUE format still belongs to the OS
   — that is a platform limit, not a style — so the control is themed and the
   app keeps formatting displayed dates itself. */
select, input[type="date"], input[type="time"], input[type="datetime-local"] {
  -webkit-appearance: none; appearance: none;
  background-color: var(--input);
  border: 1px solid var(--control-line);
  border-radius: var(--r-sm);
  color: var(--text);
  min-height: 38px;
  padding: 0 var(--sp-3);
}
/* An authored chevron at icon scale. The first replacement was two 5x5px
   gradient triangles — geometrically right, and invisible: every filter on
   the meetings screen lost the affordance the native control had. */
select {
  padding-inline-end: var(--sp-8);
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%235A6472' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><path d='M6 9l6 6 6-6'/></svg>");
  background-size: 16px 16px;
  background-position: calc(100% - var(--sp-3)) 50%;
  background-repeat: no-repeat;
}
html[dir="rtl"] select { background-position: var(--sp-3) 50%; }
html[data-theme="dark"] select {
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23ADB8C7' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><path d='M6 9l6 6 6-6'/></svg>");
}
select:hover { border-color: var(--control-line); }
input[type="date"]::-webkit-calendar-picker-indicator {
  filter: grayscale(1) opacity(.55);
  cursor: pointer;
}
html[data-theme="dark"] input[type="date"]::-webkit-calendar-picker-indicator { filter: invert(1) opacity(.6); }

.hidden { display: none !important; }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
}

/* ---------- 2. The people logo ------------------------------------------
   The bob is the login screen's one authored motion moment (§3.6). */

.logo-mark .fig-2 circle, .logo-mark .fig-2 path { fill: var(--logo-2); }

/* On the login and boot gradients the mark sits on deep blue, so the centre
   figure switches to the light ink and the cool figure to the muted one.
   Re-theming three tokens beats three more fill rules. */
#login .logo-mark, #boot .logo-mark { --logo-2: var(--login-title); --logo-3: var(--login-sub); }

.side-head .mark .fig-2 circle,
.side-head .mark .fig-2 path { fill: var(--primary); }

/* Plays ONCE on entry. It looped for the life of the login screen, which is
   an infinite animation running while someone types a password — and this
   file already argues, about the offline dot, that an effect which can be
   removed without loss should be. The same rule binds the logo. */
.logo-anim .fig-1 { animation: sfc-bob 2.6s var(--ease) 1; }
.logo-anim .fig-2 { animation: sfc-bob 2.6s var(--ease) .35s 1; }
.logo-anim .fig-3 { animation: sfc-bob 2.6s var(--ease) .7s 1; }

@keyframes sfc-bob {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-4px); }
}

/* ---------- 3. Boot splash ----------------------------------------------
   Held up until auth state is known so a signed-in person never sees the
   login form flash past on refresh (§16.2). */

#boot {
  position: fixed; inset: 0; z-index: 110;
  display: grid; place-items: center;
  background: var(--login-grad);
  transition: opacity var(--dur-slow) var(--ease);
}
#boot.is-out { opacity: 0; pointer-events: none; }
.boot-inner { display: grid; place-items: center; gap: var(--sp-5); }
.boot-inner svg { width: 104px; height: 70px; }
.boot-track {
  width: 132px; height: 3px; border-radius: var(--r-pill);
  background: var(--boot-track); overflow: hidden;
}
.boot-track i {
  display: block; width: 40%; height: 100%;
  border-radius: var(--r-pill); background: var(--accent);
  animation: sfc-slide 1.1s var(--ease) infinite;
}
@keyframes sfc-slide {
  from { transform: translateX(-110%); }
  to   { transform: translateX(360%); }
}

/* ---------- 4. Login (spec §16) ------------------------------------------
   The one place the sadu speaks at full scale (§3.5): a single large
   statement over the blue, not a repeating texture. */

#login {
  position: fixed; inset: 0; z-index: 100;
  display: grid; place-items: center;
  padding: var(--sp-6);
  padding-top: max(var(--sp-6), env(safe-area-inset-top));
  background: var(--login-grad);
  overflow-y: auto;
  overscroll-behavior: contain;
}
/* ONE statement, not a texture (§3.5) — hence no-repeat. Without it the tile
   wallpapers the screen, which is the exact thing the decision ruled out. */
#login::after {
  content: ""; position: absolute; inset: 0;
  background-image: var(--sadu);
  background-repeat: no-repeat;
  background-size: min(560px, 92vw) auto;
  background-position: center 8%;
  opacity: var(--sadu-opacity-login);
  pointer-events: none;
}

.login-card {
  position: relative; z-index: 2;
  width: 100%; max-width: 408px;
  background: var(--login-card);
  border: 1px solid var(--login-card-line);
  border-radius: var(--r-xl);
  padding: var(--sp-8) var(--sp-6) var(--sp-6);
  box-shadow: var(--shadow-lg);
  animation: sfc-rise .5s var(--ease-out) both;
}
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .login-card { -webkit-backdrop-filter: blur(var(--blur-strong)); backdrop-filter: blur(var(--blur-strong)); }
}
@keyframes sfc-rise {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: none; }
}

.login-logo { display: grid; place-items: center; margin-bottom: var(--sp-4); }
.login-logo svg { width: 88px; height: 60px; }

/* More space above a heading than below it. */
.login-card h1 {
  margin: 0;
  text-align: center;
  font-size: var(--fs-900);
  font-weight: 800;
  line-height: var(--lh-tight);
  letter-spacing: var(--tracking-tight);
  color: var(--login-title);
  text-wrap: balance;
}
.login-card .login-sub {
  margin: var(--sp-2) 0 var(--sp-6);
  text-align: center;
  font-size: var(--fs-300);
  color: var(--login-sub);
}

.field { margin-bottom: var(--sp-3); }
.field label {
  display: block;
  margin-bottom: var(--sp-1);
  font-size: var(--fs-200);
  font-weight: 600;
  color: var(--login-sub);
}
.field input {
  width: 100%;
  background: var(--login-field);
  border: 1px solid var(--login-field-line);
  color: var(--login-field-ink);
  border-radius: var(--r);
  padding: var(--sp-3);
  font-size: var(--fs-600);
  transition: border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
}
.field input::placeholder { color: var(--login-foot); opacity: 1; }
.field input:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--focus-glow);
}

/* The sign-in button is apricot on blue: the one place the accent leads,
   because it is the only action on the screen. */
.btn-gold {
  width: 100%;
  margin-top: var(--sp-4);
  background: var(--accent);
  color: var(--on-accent);
  border-radius: var(--r);
  padding: var(--sp-3);
  min-height: 46px;
  font-size: var(--fs-500);
  font-weight: 800;
  letter-spacing: .01em;
  transition: filter var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}
.btn-gold:hover  { filter: brightness(1.05); }
.btn-gold:active { transform: scale(.985); transition-duration: var(--dur-press); }
.btn-gold:disabled, .btn-gold[aria-busy="true"] { opacity: .6; cursor: default; transform: none; }

.form-error {
  display: flex; align-items: center; gap: var(--sp-2);
  margin-top: var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--r-sm);
  background: var(--login-error);
  border: 1px solid var(--login-card-line);
  color: var(--login-title);
  font-size: var(--fs-300);
}

.login-foot {
  margin-top: var(--sp-5);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--login-rule);
  text-align: center;
  font-size: var(--fs-200);
  color: var(--login-foot);
}
.login-foot b { color: var(--login-title); font-weight: 700; }

/* BUG: these two moved when the language changed — twice over. The row is a
   flex line, so RTL reversed the ORDER and the buttons swapped sides; and the
   language label changes width ("EN" vs "ع"), so a centred row re-centred
   around it.
   Deliberate anchor: these are chrome, not content. They do not mirror. The
   row is forced LTR so theme-then-language is the order in both languages, and
   each button is a fixed square so a label of any width changes nothing. */
.login-tools {
  display: flex; direction: ltr;
  justify-content: center; gap: var(--sp-2);
  margin-top: var(--sp-4);
}
.login-tool {
  display: grid; place-items: center;
  width: 44px; height: 44px;      /* fixed, so "EN" and "ع" occupy the same box */
  min-width: 44px; min-height: 44px;
  border-radius: var(--r-pill);
  background: var(--login-tool);
  border: 1px solid var(--login-tool-line);
  color: var(--login-tool-ink);
  font-size: var(--fs-200); font-weight: 700;
  transition: background var(--dur-fast) var(--ease);
}
.login-tool svg { width: 18px; height: 18px; }
.login-tool:hover { background: var(--login-tool-hover); }

/* ---------- 5. App frame -------------------------------------------------- */

#app { display: none; min-height: 100dvh; }
/* The first column is a SPACER for the fixed sidebar, not the sidebar itself
   — it reserves the width so the content never runs underneath. */
#app.is-on { display: grid; grid-template-columns: var(--side) 1fr; }

.main {
  min-width: 0;
  display: flex; flex-direction: column;
  min-height: 100dvh;
}

.content {
  flex: 1;
  padding: var(--sp-6) var(--sp-6) var(--sp-10);
  min-width: 0;
}
.content.is-entering { animation: sfc-view-fade var(--dur-slow) var(--ease-out) both; }
@keyframes sfc-view-fade {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}

/* ---------- 6. Sidebar ----------------------------------------------------
   Frosted, not a solid slab. Its backdrop is only ever the page ground, so
   it can be genuinely glassy at 0.72 and still carry secondary text (§3.2). */

/* THE SHELL DOES NOT SCROLL. The content does.

   This was briefly broken by a second `.side { position: relative }` rule
   added later in this file for the sadu: same specificity, later in source,
   so it won and the sidebar became an ordinary grid item that scrolled away
   entirely — no nav at all past the fold, on every screen. Two design reviews
   and 320 matrix assertions missed it because every capture was at scroll-top.

   `sticky`, NOT `fixed`. I tried fixed first and it was worse: a fixed grid
   child is out of flow, so `.main` auto-placed into the FIRST column and every
   screen's content collapsed to 264px. Sticky keeps the sidebar in its grid
   track — the track is what reserves the width — and `height: 100dvh` plus
   `top: 0` is what makes it stay put. That combination was always correct; the
   only thing wrong with it was the override below it. */
.side {
  position: sticky; top: 0;
  height: 100dvh;
  display: flex; flex-direction: column;
  background: var(--side-solid);
  border-inline-end: 1px solid var(--line);
  padding: var(--sp-4) var(--sp-3);
  overflow-y: auto;
  overscroll-behavior: contain;
  /* The nav is taller on some views than others (counts appear, groups differ),
     so without this the sidebar itself twitched on every tab switch. */
  scrollbar-gutter: stable;
}
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .side {
    background: var(--side-bg);
    -webkit-backdrop-filter: blur(var(--blur)) saturate(150%);
    backdrop-filter: blur(var(--blur)) saturate(150%);
    will-change: backdrop-filter;
  }
}

.side-head {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-2) var(--sp-5);
}
.side-head .mark { display: grid; place-items: center; flex: none; }
.side-head .mark svg { width: 40px; height: 28px; }
.side-head .who { display: flex; flex-direction: column; min-width: 0; }
.side-head h1 {
  margin: 0;
  font-size: var(--fs-500); font-weight: 800;
  letter-spacing: var(--tracking-tight);
  color: var(--side-ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.side-head span {
  font-size: var(--fs-200);
  color: var(--side-label);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.side-new { padding: 0 var(--sp-1) var(--sp-5); }

.btn-new {
  width: 100%;
  display: flex; align-items: center; justify-content: center; gap: var(--sp-2);
  min-height: 42px;
  border-radius: var(--r);
  background: var(--primary);
  color: var(--on-primary);
  font-size: var(--fs-400); font-weight: 700;
  box-shadow: var(--shadow-sm);
  transition: background var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}
.btn-new svg { width: 18px; height: 18px; }
.btn-new:hover  { background: var(--primary-strong); }
.btn-new:active { transform: scale(.985); transition-duration: var(--dur-press); }

.nav { display: flex; flex-direction: column; gap: var(--sp-4); flex: 1; }
.nav-group + .nav-group { margin-top: 0; }

.nav-label {
  display: block;
  padding: 0 var(--sp-3) var(--sp-2);
  font-size: var(--fs-100); font-weight: 700;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--side-label);
}

.nav-item {
  position: relative;
  width: 100%;
  display: flex; align-items: center; gap: var(--sp-3);
  min-height: 40px;
  padding: 0 var(--sp-3);
  border-radius: var(--r);
  color: var(--side-ink-2);
  font-size: var(--fs-400); font-weight: 600;
  text-align: start;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.nav-item + .nav-item { margin-top: 2px; }
.nav-item svg { width: 18px; height: 18px; opacity: .8; }
.nav-item .label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nav-item:hover { background: var(--side-hover); color: var(--side-ink); }

/* Active is a filled shape, not a 3px edge stripe. Logical properties mean
   RTL needs no special case — the Phase 1 rule did, and it was fragile.

   The fill is now SOLID primary rather than a 10% tint. Measured, the tint
   separated selected from unselected by 1.17:1 on fill and 1.16:1 on ink —
   below the 3:1 WCAG 1.4.11 wants of a state indicator, and the two channels
   were not independent enough to cover for each other. A tint cannot fix it:
   any ground pale enough to carry --primary ink at 4.5:1 is, by arithmetic,
   too pale to reach 3:1 against the sidebar. So the pill fills and the ink
   flips — 6.6:1 fill and 7.0:1 ink, and it matches the segmented control. */
.nav-item.is-on {
  background: var(--primary);
  color: var(--on-primary);
  font-weight: 700;
}
.nav-item.is-on svg { opacity: 1; }
.nav-item.is-on:hover { background: var(--primary-strong); color: var(--on-primary); }
.nav-item.is-on .count { background: var(--on-primary-soft); color: var(--on-primary); }

.nav-item .count {
  min-width: 22px; height: 20px;
  padding-inline: 6px;
  display: grid; place-items: center;
  border-radius: var(--r-pill);
  background: var(--tint-mute);
  color: var(--ink-mute);
  font-size: var(--fs-100); font-weight: 700;
}
.nav-item .count.is-warn { background: var(--tint-accent); color: var(--ink-accent); }

/* The moving indicator is retired: the filled shape reads as the state, and a
   travelling pill was motion without meaning (§3.6). Kept as a no-op so any
   stale reference cannot paint a stray rectangle. */
.nav-pill { display: none; }

.side-foot { padding-top: var(--sp-4); border-top: 1px solid var(--line-soft); }

/* width:auto, NOT 100%. These are two icon buttons sitting beside .me in the
   footer row; at 100% each they ate the row and squeezed the person's name to
   two characters while their role wrapped underneath. */
.me-btn {
  width: auto; flex: none;
  min-width: 40px; min-height: 40px;
  display: grid; place-items: center;
  border-radius: var(--r);
  color: var(--side-ink-2);
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.side-foot { display: flex; align-items: center; gap: var(--sp-1); }
.side-foot .me { flex: 1; min-width: 0; }
.me-btn:hover, .me-btn.is-on { background: var(--side-hover); color: var(--side-ink); }

.me { display: flex; align-items: center; gap: var(--sp-3); min-width: 0; width: 100%; }
.me .avatar {
  width: 34px; height: 34px; flex: none;
  display: grid; place-items: center;
  border-radius: var(--r-pill);
  background: var(--primary);
  color: var(--on-primary);
  font-size: var(--fs-200); font-weight: 800;
}
/* An explicit column: the Arabic name and the role ran together when this
   was a span (Phase 1). */
.me .id { display: flex; flex-direction: column; min-width: 0; text-align: start; }
.me .name {
  font-size: var(--fs-400); font-weight: 700; color: var(--side-ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.me .role { font-size: var(--fs-200); color: var(--side-label); }

@media (max-width: 900px) { .me-btn { width: 44px; height: 44px; } }

/* ---------- 7. Top bar ----------------------------------------------------
   The ONLY chrome with unbounded content scrolling under it, so it is denser
   than the sidebar (0.82 light / 0.72 dark) and carries no secondary text —
   at lower opacity, text-2 over a coloured card measures 3.85:1 (§3.2). */

.topbar {
  position: sticky; top: 0; z-index: 40;
  height: var(--topbar-h);
  display: flex; align-items: center; gap: var(--sp-3);
  padding-inline: var(--sp-6);
  padding-top: env(safe-area-inset-top);
  background: var(--glass-solid);
  /* A DEFINED EDGE, not a glass hairline. Content scrolls under this bar and
     --glass-line is 0.09 alpha in dark — rows passed behind it and reappeared
     with nothing marking where the chrome ended. The bar is frosted by design
     (§3.2), so the boundary has to be drawn rather than implied. */
  border-bottom: 1px solid var(--line);
  box-shadow: var(--shadow-sm);
}
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .topbar {
    background: var(--glass-bar);
    -webkit-backdrop-filter: blur(var(--blur-strong)) saturate(160%);
    backdrop-filter: blur(var(--blur-strong)) saturate(160%);
    /* Own compositing layer. Measured: a press on a 130x38 button in the
       content area produced full-viewport paints that re-rastered this bar and
       the sidebar — 5.7ms vs 3.0ms with the blur off. Trivial on a desktop,
       and exactly the mechanism that costs frames on a phone (§9). Promoting
       these two surfaces keeps a content repaint out of them. */
    will-change: backdrop-filter;
  }
}

/* `.burger.icon-btn`, not `.burger`. Both are one class, and `.icon-btn`
   sets `display: grid` further down this file — at equal specificity the
   later rule wins, so a bare `.burger` selector puts the menu button on every
   desktop. §3.6 names this trap; it caught this rebuild too. */
/* Scrolled state: the bar's edge is a hairline at rest and gains a shadow
   once content is actually behind it, so the glass reads as a plane rather
   than as paint. app.js toggles .is-scrolled. */
.topbar.is-scrolled { box-shadow: var(--shadow-sm); border-bottom-color: var(--line); }

.burger.icon-btn { display: none; }

.topbar-actions { margin-inline-start: auto; display: flex; align-items: center; gap: var(--sp-2); }

.icon-btn {
  position: relative;
  width: 40px; height: 40px;
  display: grid; place-items: center;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r-pill);
  color: var(--text-2);
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.icon-btn svg { width: 19px; height: 19px; }
.icon-btn:hover { background: var(--surface-3); color: var(--text); }
.icon-btn:active { transform: scale(.94); transition-duration: var(--dur-press); }
.icon-btn[aria-expanded="true"] { background: var(--primary-soft); color: var(--primary); }
.icon-btn.lang { font-size: var(--fs-200); font-weight: 800; letter-spacing: .03em; }

.icon-btn .badge {
  position: absolute; top: 2px; inset-inline-end: 2px;
  min-width: 17px; height: 17px; padding-inline: 4px;
  display: grid; place-items: center;
  border-radius: var(--r-pill);
  background: var(--accent);
  color: var(--on-accent);
  font-size: var(--fs-075); font-weight: 800;
  border: 2px solid var(--glass-solid);
}

/* Offline pill (§10) — a state, not an error. Loud enough to notice, quiet
   enough not to look like a dialog, and it clears itself. */
.offline-pill {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  height: 32px; padding-inline: var(--sp-3);
  border: 1px solid var(--edge-accent);
  border-radius: var(--r-pill);
  background: var(--tint-accent);
  color: var(--ink-accent);
  font-size: var(--fs-300); font-weight: 700;
  white-space: nowrap;
  animation: pill-in var(--dur) var(--ease-out);
}
.offline-pill svg { width: 16px; height: 16px; }
/* A STATIC dot. It pulsed at first, to read as "still trying" — but the pill
   only exists while the app is offline, so its presence already carries that,
   and a permanently animating dot in the chrome is decorative liveness.
   If an effect can be removed without loss, remove it. */
.offline-pill::before {
  content: ''; width: 7px; height: 7px; border-radius: 50%;
  background: currentColor;
}
@keyframes pill-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .offline-pill { animation: none; } }
@media (max-width: 560px) {
  .offline-pill { padding-inline: var(--sp-2); }
  .offline-pill #offline-text { display: none; }
}

/* ---------- 8. Popovers, menus -------------------------------------------- */

.pop, .menu {
  position: fixed; z-index: 60;
  border-radius: var(--r-lg);
  background: var(--glass-solid);
  border: 1px solid var(--line);
  box-shadow: var(--shadow-lg);
  animation: sfc-pop var(--dur) var(--ease-out) both;
  overflow: hidden;
}
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .pop, .menu {
    background: var(--glass-panel);
    -webkit-backdrop-filter: blur(var(--blur-strong)) saturate(160%);
    backdrop-filter: blur(var(--blur-strong)) saturate(160%);
  }
}
@keyframes sfc-pop {
  from { opacity: 0; transform: translateY(-6px) scale(.985); }
  to   { opacity: 1; transform: none; }
}
/* The bell panel and the + menu are torn out of the DOM on close, so they
   need an animation rather than a transition, plus a JS-side delay before
   the removal. `both` holds the end frame so there is no flash back to
   opaque in the gap between animationend and the innerHTML clear. */
.pop.is-closing, .menu.is-closing {
  animation: sfc-pop-out var(--dur-exit) var(--ease-in) both;
  pointer-events: none;
}
@keyframes sfc-pop-out {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: translateY(-4px) scale(.99); }
}

.pop {
  top: calc(var(--topbar-h) + var(--sp-2));
  inset-inline-end: var(--sp-6);
  width: min(380px, calc(100vw - var(--sp-8)));
  max-height: min(70dvh, 560px);
  display: flex; flex-direction: column;
}
.pop-head {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-4);
  border-bottom: 1px solid var(--line-soft);
}
.pop-head svg { color: var(--text-2); }
.pop-head h3 { margin: 0; font-size: var(--fs-400); font-weight: 800; }
.pop-head .mark-all {
  margin-inline-start: auto;
  font-size: var(--fs-200); font-weight: 700;
  color: var(--primary);
  padding: var(--sp-1) var(--sp-2);
  border-radius: var(--r-sm);
}
.pop-head .mark-all:hover { background: var(--primary-soft); }

.pop-list { overflow-y: auto; overscroll-behavior: contain; scrollbar-gutter: stable; }

.notif {
  width: 100%;
  display: flex; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  text-align: start;
  border-bottom: 1px solid var(--line-soft);
  transition: background var(--dur-fast) var(--ease);
}
.notif:last-child { border-bottom: 0; }
.notif:hover { background: var(--surface-2); }
.notif.is-unread { background: var(--primary-soft); }
.notif.is-unread:hover { background: var(--primary-soft); filter: brightness(.98); }
.notif .icon {
  width: 32px; height: 32px; flex: none;
  display: grid; place-items: center;
  border-radius: var(--r-sm);
}
.notif .icon svg { width: 16px; height: 16px; }
.notif .body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.notif .title { font-size: var(--fs-400); font-weight: 700; color: var(--text); }
.notif .desc  { font-size: var(--fs-300); color: var(--text-2); }
.notif .when  { font-size: var(--fs-200); color: var(--text-3); }

.menu { min-width: 220px; padding: var(--sp-2); }
.menu-item {
  width: 100%;
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--r-sm);
  font-size: var(--fs-400); font-weight: 600;
  text-align: start;
  color: var(--text);
}
.menu-item svg { width: 17px; height: 17px; color: var(--text-2); }
.menu-item:hover { background: var(--primary-soft); color: var(--primary); }
.menu-item:hover svg { color: var(--primary); }

/* Design mode is loud on purpose. A pass spent judging layout against a
   fixture is useful; one spent judging it against a fixture you forgot about
   is worse than useless. The bar is fixed, above everything, and unmissable
   in either theme. */
/* And it does not print. It is fixed chrome, not part of any document, and
   @media print hides #app rather than the body pseudo-element that sits over
   it — so a report generated with design mode on came out with an apricot
   banner across the top of page one. */
@media print { body.is-design-mode::before { display: none !important; } }
body.is-design-mode::before {
  content: "DESIGN MODE — fixture data, writes refused";
  position: fixed; inset-block-start: 0; inset-inline: 0; z-index: 100;
  height: 22px;
  display: grid; place-items: center;
  background: var(--accent);
  color: var(--on-accent);
  font-size: var(--fs-075); font-weight: 800; letter-spacing: .04em;
  pointer-events: none;
}
body.is-design-mode #app { padding-block-start: 22px; }

/* ---------- 9. Scrim ------------------------------------------------------ */

/* Two transitions, not one: the base rule is what runs when `is-on` comes
   OFF, so the exit is the quicker of the pair. Same trick on .drawer. */
.scrim {
  position: fixed; inset: 0; z-index: 50;
  background: var(--scrim);
  opacity: 0; pointer-events: none;
  transition: opacity var(--dur-exit) var(--ease-in);
}
.scrim.is-on { opacity: 1; pointer-events: auto; transition: opacity var(--dur) var(--ease); }

/* ---------- 10. Buttons ---------------------------------------------------- */

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2);
  min-height: 38px;
  padding: 0 var(--sp-4);
  border-radius: var(--r);
  border: 1px solid var(--control-line);
  background: var(--surface);
  color: var(--text);
  font-size: var(--fs-400); font-weight: 700;
  white-space: nowrap;
  box-shadow: var(--shadow-sm);
  transition: background var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease),
              transform var(--dur-fast) var(--ease);
}
.btn svg { width: 17px; height: 17px; }
.btn:hover  { background: var(--surface-2); border-color: var(--text-3); }
/* SNAP IN, EASE OUT. The press used to ramp over 140ms, which is what made an
   instant control feel slow — measured: :active landed in ~25ms but took a
   further 140ms to look pressed. Zero on the way down, eased on the way back. */
.btn:active { transform: scale(.985); transition-duration: var(--dur-press); }
.btn:disabled { opacity: .5; cursor: default; transform: none; }

.btn.primary {
  background: var(--primary);
  border-color: transparent;
  color: var(--on-primary);
}
.btn.primary:hover { background: var(--primary-strong); }

/* Ghost keeps its transparent fill — that is the whole point of the tier —
   but NOT a transparent edge. With both gone the control drew literally
   nothing at rest: measured boundary 1.00:1 in both themes, which is what
   made "Edit" and "Archived" on the Projects cards read as stray words. The
   label was never the problem; it measures 5.33:1 light and 8.07:1 dark. */
.btn.ghost { background: transparent; border-color: var(--control-line); box-shadow: none; color: var(--text-2); }
.btn.ghost:hover { background: var(--surface-3); color: var(--text); border-color: var(--text-3); }

/* ---------- 11. Chips (§3.3) ----------------------------------------------
   Colour AND glyph AND text. Never colour alone — the reports get
   photocopied, and hue alone excludes anyone who cannot separate them. */

.chip {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 2px 9px;
  border-radius: var(--r-pill);
  border: 1px solid transparent;
  font-size: var(--fs-100); font-weight: 700;
  line-height: 1.7;
  white-space: nowrap;
}
.chip .glyph { font-size: var(--fs-075); line-height: 1; opacity: .9; }

.c-ok      { background: var(--tint-ok);      color: var(--ink-ok);      border-color: var(--edge-ok); }
.c-info    { background: var(--tint-info);    color: var(--ink-info);    border-color: var(--edge-info); }
.c-warn    { background: var(--tint-warn);    color: var(--ink-warn);    border-color: var(--edge-warn); }
.c-danger  { background: var(--tint-danger);  color: var(--ink-danger);  border-color: var(--edge-danger); }
.c-pending { background: var(--tint-pending); color: var(--ink-pending); border-color: var(--edge-pending); }
.c-accent  { background: var(--tint-accent);  color: var(--ink-accent);  border-color: var(--edge-accent); }
.c-mute    { background: var(--tint-mute);    color: var(--ink-mute);    border-color: var(--edge-mute); }

/* ---------- 12. Panels, page heads, empty states --------------------------- */

/* ==========================================================================
   THE SADU IS NOT AN APPLIED SIGNATURE ANY MORE (§3.30)

   Two attempts, two opposite failures. As a 6px horizontal hairline under the
   page head at 28% opacity it was invisible — a fresh reviewer had to open
   tokens.css to learn it was a motif rather than a dashed border. Moved to the
   sidebar's inner edge it became worse than invisible: MISLEADING. It sat at
   `inset-inline-end: 0` of an element with `overflow-y: auto` and
   `scrollbar-gutter: stable`, which is to say directly on the reserved scroll
   track, in the width and orientation of one — and the manager tried to drag
   it.

   The artwork was the reason. `--sadu-rule` is 96×6 HORIZONTAL chevrons; an
   8px-wide window with `repeat-y` shows a narrow crop of that pattern tiled
   downward, not a rotated band. The placement was rotated and the artwork was
   not. A vertical variant would have fixed the crop and left the thing sitting
   on a scroll track, so it is dropped rather than rebuilt.

   Better no signature than a misleading one. The app's identity has to come
   from its shapes and its language — the register, the strata, the chart, the
   one dominant object, Arabic-first — and 8.6 closed without one.

   The tokens stay: --sadu still dresses the login screen and the empty states
   (§3.5), where it is large enough to read as a motif.
   ========================================================================== */

/* ==========================================================================
   THE PAGE SHELL — one per screen, and one rule for all of them

   Most screens had no outer edge at all: content began at the top-left of the
   viewport and ran to the bottom with nothing holding it, so a page read as
   loose text ON a background rather than as a surface. Calendar's WEEK view
   was the only screen already doing it, which is why the values below are
   taken from `.wk-grid` and not invented a second time: 1px `--line`,
   `--r`, on `--surface`.

   THE ONE THING IT MUST NOT DO IS REPEAT ITSELF INSIDE. The shell goes AROUND
   a screen's own containers - Board's columns, Projects' accent rows,
   Archive's purge panel, Table's project bands - and never around each of
   them again. A visible border immediately inside a visible border with only
   padding between the two is the failure state, not the goal.

   AND AN EMPTY STATE STAYS BARE. Step 22a of the filter walk removed the box
   from Board's filtered-empty message on purpose. The PAGE gets an edge; the
   message inside it does not get a second one, and a screen does not lose its
   shell for being empty.

   Padding is the shell's own, because `.wk-grid` has none - its cells run to
   the frame. sp-6 matches the inset `.content` already uses, so the frame
   sits on the same rhythm as the page it is inside.

   THE SHELL IS AN EDGE, NOT A FILL, AND THAT WAS DECIDED THE HARD WAY. It
   took `--surface` first, copied straight off `.wk-grid`. But `.wk-grid`
   holds CELLS that run to its frame; every other screen holds CARDS, and
   `.meeting-row` - the reference for all of them - has no border at all. It
   reads as a card because white sits on cool paper, which is what the `--bg`
   token comment has said since 8.5: "cool paper - white cards read as raised
   on it." A `--surface` shell removes the paper, so a Meetings-value card
   inside one lands at 1.000:1 and disappears.

   That is not a prediction. It happened first to My Day's task-row hover,
   which painted `--surface` on a `--surface` shell and moved nothing at all.
   Six more screens were queued behind it.

   So the interior stays `--bg` and the shell contributes the border and the
   radius only. The cost is real and was accepted deliberately: the frame does
   not lighten the page. An edge is what was missing; a brighter interior was
   never the ask.

   `--shell-pad` is a variable rather than a literal so a full-bleed child can
   negate it without knowing the number - see `.md-unclosed`. The one hardcoded
   `calc(var(--sp-6) * -1)` left in the build is `.blocked-strip`, which is
   correct only until Board gets a shell. */
.page-shell {
  --shell-pad: var(--sp-6);
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: var(--r);
  padding: var(--sp-5) var(--shell-pad);
  min-width: 0;
}
@media (max-width: 700px) {
  /* The frame stays - it is the point - but it stops eating a phone's width. */
  .page-shell { --shell-pad: var(--sp-4); padding-block: var(--sp-4); }
}

/* ==========================================================================
   LEVEL 2 — THE PAGE HEADER

   One treatment, every screen. It holds what the screen already opens with -
   a count, a navigator, a tab row, the actions - and separates it from the
   content by A RULE, never a second border. A bordered box at the top of a
   bordered shell is the box-in-box this phase exists to remove.

   IT BLEEDS TO THE SHELL, like the band, so the rule spans the full inner
   width. A rule that stops 24px short of each edge reads as an underline
   under the controls rather than a division of the page, and it puts two
   parallel horizontal terminations near the shell's corners.

   WHAT IT DELIBERATELY DOES NOT DO IS ADD A TITLE - see the note in views.css
   beside .md-open. §3.28 removed nine identical title blocks and the sidebar
   already names the view; this gives their opening objects a container, it
   does not put the blocks back.

   A screen whose opening object needs more presence adds it in TYPE, not in a
   second container - which is My Day, and only My Day. */
/* ZERO BY DEFAULT, so a header OUTSIDE a shell is a plain rule rather than an
   accident. `.page-head` negates the shell's inset to bleed; with no shell and
   no default, `--shell-pad` resolved to nothing, `calc()` became invalid, and
   the margin fell back to zero — the right answer for the wrong reason, and
   the kind of thing that stops being right the day someone adds a fallback.
   Calendar is the screen that needs it: its month grid carries its own frame,
   so it has no shell for the header to bleed against. */
:root { --shell-pad: 0px; }

.page-head {
  margin-inline: calc(var(--shell-pad) * -1);
  padding-inline: var(--shell-pad);
  padding-block-end: var(--sp-4);
  border-block-end: 1px solid var(--line);
}
/* WHAT THE SCREEN IS SHOWING, on the leading side. It was `.reg-count`, on
   Table only, while every other header opened with a navigator or a tab row
   and Timeline's opened with nothing at all — a lone button at the trailing
   edge over a stretch of air is a button with a line under it, not a
   container. A count is the one thing every list screen can always say. */
.page-head-count {
  font-size: var(--fs-100); font-weight: 800;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-3);
}

/* The trailing rail: whatever the screen can DO, at the end of the header row
   in both writing directions. One `auto` margin in a row, never two - two
   split the free space between them and the things they push stop being
   adjacent, which is why Calendar's Today button moved INTO this rail rather
   than keeping an auto margin of its own beside it. */
.page-head-acts {
  margin-inline-start: auto;
  display: flex; align-items: center; gap: var(--sp-2);
  flex: none;
}

/* THE BAND — a condition notice that runs the full width of the page.

   Three of these existed before this class did, each repeating the same six
   declarations: My Day's unclosed count, Board's filter strip and Timeline's
   hidden-task note. `.blocked-strip` was the original and its comment already
   said the shape out loud - a full-bleed band, NOT a rounded tinted card - so
   the fourth copy was the moment to make it one thing.

   It bleeds to the shell's inner edge, so its sides are bounded by the frame
   and its own rules are only above and below. A rounded box would run a second
   border 24px inside the shell's, which is the box-in-box this phase removes.

   The TINT and the EDGE are the caller's: a band is warn, accent or info
   depending on what it says, and §3.23 requires whichever ramp edge goes with
   the fill. A pressable band needs more than a ramp edge - see `.md-unclosed`,
   where §3.7's 3:1 applies because it is a button. */
.page-band {
  display: flex; align-items: center; gap: var(--sp-2);
  width: calc(100% + var(--shell-pad) * 2);
  margin-inline: calc(var(--shell-pad) * -1);
  padding: var(--sp-3) var(--shell-pad);
  font-size: var(--fs-200);
  color: var(--text);
  text-align: start;
}

/* The row itself, held back for two commits because Calendar's navigator was
   already one and a second flex declaration would either have lost to
   `.cal-nav` by source order or changed the WEEK view. Board is the first
   screen that opens a header from nothing, so it lands here. */
.page-head-row {
  display: flex; align-items: center; gap: var(--sp-4);
  flex-wrap: wrap;
  min-width: 0;
}

.panel {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
}
.panel + .panel { margin-top: var(--sp-5); }
.panel-head {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-4) var(--sp-5);
  border-bottom: 1px solid var(--line-soft);
}
.panel-head svg { color: var(--text-2); }
.panel-head h3 { margin: 0; font-size: var(--fs-500); font-weight: 800; }
.panel-body { padding: var(--sp-5); }

/* The sadu's second and last appearance: behind an empty state, very quiet.
   Never behind working content (§3.5). */
.empty {
  position: relative;
  display: grid; place-items: center; gap: var(--sp-2);
  padding: var(--sp-12) var(--sp-6);
  text-align: center;
}
.empty::before {
  content: ""; position: absolute; inset: 0;
  background-image: var(--sadu);
  background-repeat: no-repeat;
  background-size: 320px auto;
  background-position: center;
  opacity: var(--sadu-opacity-empty);
  pointer-events: none;
}
.empty svg { width: 30px; height: 30px; color: var(--text-3); position: relative; }
.empty h4 { margin: var(--sp-1) 0 0; font-size: var(--fs-500); font-weight: 800; position: relative; }
.empty p  { margin: 0; font-size: var(--fs-300); color: var(--text-2); max-width: 46ch; position: relative; }

/* ---------- 13. Toasts ------------------------------------------------------
   Anchored to the inline END. Anchoring to the start put them over the
   sidebar in both directions (Phase 1). */

#toasts {
  position: fixed; z-index: 90;
  inset-block-end: calc(var(--sp-5) + env(safe-area-inset-bottom));
  inset-inline-end: var(--sp-5);
  display: flex; flex-direction: column; gap: var(--sp-2);
  pointer-events: none;
}
.toast {
  display: flex; align-items: center; gap: var(--sp-2);
  max-width: min(420px, calc(100vw - var(--sp-8)));
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--r);
  background: var(--surface);
  border: 1px solid var(--line);
  box-shadow: var(--shadow-lg);
  font-size: var(--fs-300); font-weight: 600;
  color: var(--text);
  animation: sfc-toast var(--dur) var(--ease-out) both;
  pointer-events: auto;
}
/* Tone lives in the icon and a hairline, not a 3px edge stripe: a thick
   coloured border on one side is the most recognisable tell there is, and the
   brief reopened it explicitly. Colour stays redundant with the icon. */
.toast svg { width: 17px; height: 17px; color: var(--text-2); }
.toast.ok   { border-color: var(--edge-ok); }
.toast.warn { border-color: var(--edge-warn); }
.toast.bad  { border-color: var(--edge-danger); }
.toast.ok   svg { color: var(--ink-ok); }
.toast.warn svg { color: var(--ink-warn); }
.toast.bad  svg { color: var(--ink-danger); }
@keyframes sfc-toast {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}

/* ---------- 14. FAB + mobile tab bar ---------------------------------------- */

.fab {
  position: fixed;
  inset-block-end: var(--sp-6);
  inset-inline-end: var(--sp-6);
  z-index: 35;
  display: none;
  place-items: center;
  width: 56px; height: 56px;
  border-radius: var(--r-pill);
  background: var(--primary);
  color: var(--on-primary);
  box-shadow: var(--shadow-lg);
  transition: transform var(--dur-fast) var(--ease);
}
.fab:active { transform: scale(.96); transition-duration: var(--dur-press); }
.fab svg { width: 24px; height: 24px; }

.tabbar {
  display: none;
  position: fixed; inset-inline: 0; inset-block-end: 0; z-index: 45;
  height: calc(var(--tabbar-h) + env(safe-area-inset-bottom));
  padding-bottom: env(safe-area-inset-bottom);
  background: var(--glass-solid);
  border-top: 1px solid var(--glass-line);
}
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .tabbar {
    background: var(--glass-bar);
    -webkit-backdrop-filter: blur(var(--blur-strong)) saturate(160%);
    backdrop-filter: blur(var(--blur-strong)) saturate(160%);
  }
}
.tabbar a {
  display: grid; place-items: center; gap: 2px;
  min-height: 44px;
  font-size: var(--fs-100); font-weight: 700;
  color: var(--text-3);
  text-decoration: none;
}
.tabbar a svg { width: 21px; height: 21px; }
/* Colour alone separated the current tab from the rest at 1.16:1 — the same
   arithmetic that failed the sidebar. A solid bar across the top of the tab
   is a second, non-colour channel and measures 6.6:1 against the chrome. */
.tabbar a { position: relative; }
.tabbar a.is-on { color: var(--primary); }
.tabbar a.is-on svg { color: var(--primary); }
.tabbar a.is-on::before {
  content: ""; position: absolute; inset-block-start: 0; inset-inline: 22%;
  height: 3px; border-radius: 0 0 var(--r-pill) var(--r-pill);
  background: var(--primary);
}

/* ---------- 15. Responsive --------------------------------------------------
   §17: 44px targets, 16px inputs, 100dvh, safe-area insets, and the burger
   hidden on desktop — which is a specificity fight with .icon-btn, so it is
   written as .burger.icon-btn on purpose. */

/* §17's 44px floor applies to every control on a touch device, not just the
   one that was noticed. Pointer-coarse rather than a width query: a small
   laptop with a touchscreen needs it and a narrow desktop window does not. */
@media (pointer: coarse) {
  .btn, .icon-btn, .nav-item, .me-btn, .login-tool, .menu-item { min-height: 44px; }
  .icon-btn, .me-btn { min-width: 44px; }
}

@media (max-width: 900px) {
  #app.is-on { grid-template-columns: 1fr; }

  .side {
    position: fixed; inset-block: 0; inset-inline-start: 0;
    z-index: 55;
    width: min(300px, 86vw);
    transform: translateX(-102%);
    transition: transform var(--dur-exit) var(--ease-in);
    box-shadow: var(--shadow-lg);
  }
  html[dir="rtl"] .side { transform: translateX(102%); }
  /* `.is-on`, not `.is-open` — app.js's openSidebar() adds `is-on`, and this
     rule was written against a class nothing sets, so the mobile sidebar
     never slid in and the burger appeared to do nothing. `html .` keeps the
     specificity above the RTL transform rule above, same trap as .drawer. */
  html .side.is-on { transform: none; transition: transform var(--dur) var(--ease); }

  .burger.icon-btn { display: grid; }
  .topbar { padding-inline: var(--sp-4); }

  /* Stack, or the actions squeeze the subtitle into a column of single words. */


  .content { padding: var(--sp-4) var(--sp-4) calc(var(--tabbar-h) + var(--sp-10)); }

  .tabbar { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; }
  .fab.is-available { display: grid; inset-block-end: calc(var(--tabbar-h) + var(--sp-4) + env(safe-area-inset-bottom)); }

  /* iOS zooms any field under 16px on focus. */
  input, select, textarea { font-size: var(--fs-600); }

  .pop { inset-inline: var(--sp-3); width: auto; }
}

@media (min-width: 901px) and (max-width: 1180px) {
  #app.is-on { grid-template-columns: var(--side-narrow) 1fr; }
  .side { padding-inline: var(--sp-2); }
  .side-head .who, .nav-item .label, .nav-item .count, .nav-label, .me .id { display: none; }
  .side-head, .nav-item, .me-btn { justify-content: center; }
  .btn-new { padding: 0; }
  .btn-new span { display: none; }
}

@media (display-mode: standalone) {
  .topbar { padding-top: max(env(safe-area-inset-top), var(--sp-1)); }
}

@media print {
  .side, .topbar, .fab, .tabbar, #toasts, .pop, .menu, .scrim { display: none !important; }
  #app.is-on { display: block; }
  .content { padding: 0; }
}
