/*!******************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************!*\
  !*** css ../../node_modules/next/dist/build/webpack/loaders/css-loader/src/index.js??ruleSet[1].rules[14].oneOf[2].use[1]!../../node_modules/next/dist/build/webpack/loaders/next-font-loader/index.js??ruleSet[1].rules[14].oneOf[2].use[2]!../../node_modules/next/font/google/target.css?{"path":"src/app/layout.tsx","import":"Syne","arguments":[{"subsets":["latin"],"variable":"--font-display","display":"swap"}],"variableName":"display"} ***!
  \******************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************/
/* greek */
@font-face {
  font-family: 'Syne';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url(/_next/static/media/0da54fcd0470ea43-s.woff2) format('woff2');
  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* latin-ext */
@font-face {
  font-family: 'Syne';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url(/_next/static/media/680a7121f7a85e3f-s.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Syne';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url(/_next/static/media/8a1d8947e5852e30-s.p.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}@font-face {font-family: 'Syne Fallback';src: local("Arial");ascent-override: 93.93%;descent-override: 27.93%;line-gap-override: 0.00%;size-adjust: 98.47%
}.__className_744198 {font-family: 'Syne', 'Syne Fallback';font-style: normal
}.__variable_744198 {--font-display: 'Syne', 'Syne Fallback'
}

/*!*********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************!*\
  !*** css ../../node_modules/next/dist/build/webpack/loaders/css-loader/src/index.js??ruleSet[1].rules[14].oneOf[2].use[1]!../../node_modules/next/dist/build/webpack/loaders/next-font-loader/index.js??ruleSet[1].rules[14].oneOf[2].use[2]!../../node_modules/next/font/google/target.css?{"path":"src/app/layout.tsx","import":"Source_Sans_3","arguments":[{"subsets":["latin"],"variable":"--font-body","display":"swap"}],"variableName":"body"} ***!
  \*********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************/
/* cyrillic-ext */
@font-face {
  font-family: 'Source Sans 3';
  font-style: normal;
  font-weight: 200 900;
  font-display: swap;
  src: url(/_next/static/media/88da59c891d37117-s.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
  font-family: 'Source Sans 3';
  font-style: normal;
  font-weight: 200 900;
  font-display: swap;
  src: url(/_next/static/media/1e377d93cbdd6b44-s.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
  font-family: 'Source Sans 3';
  font-style: normal;
  font-weight: 200 900;
  font-display: swap;
  src: url(/_next/static/media/36d711d427a06e51-s.woff2) format('woff2');
  unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
  font-family: 'Source Sans 3';
  font-style: normal;
  font-weight: 200 900;
  font-display: swap;
  src: url(/_next/static/media/f1f7cdf240180e28-s.woff2) format('woff2');
  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* vietnamese */
@font-face {
  font-family: 'Source Sans 3';
  font-style: normal;
  font-weight: 200 900;
  font-display: swap;
  src: url(/_next/static/media/55fbdf033607a0e1-s.woff2) format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
  font-family: 'Source Sans 3';
  font-style: normal;
  font-weight: 200 900;
  font-display: swap;
  src: url(/_next/static/media/b6a6f0b43d027304-s.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Source Sans 3';
  font-style: normal;
  font-weight: 200 900;
  font-display: swap;
  src: url(/_next/static/media/9cf9c6e84ed13b5e-s.p.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}@font-face {font-family: 'Source Sans 3 Fallback';src: local("Arial");ascent-override: 109.21%;descent-override: 42.66%;line-gap-override: 0.00%;size-adjust: 93.76%
}.__className_1fdbab {font-family: 'Source Sans 3', 'Source Sans 3 Fallback';font-style: normal
}.__variable_1fdbab {--font-body: 'Source Sans 3', 'Source Sans 3 Fallback'
}

/*!*************************************************************************************************************************************************************************************************************************************************************************!*\
  !*** css ../../node_modules/next/dist/build/webpack/loaders/css-loader/src/index.js??ruleSet[1].rules[14].oneOf[10].use[2]!../../node_modules/next/dist/build/webpack/loaders/postcss-loader/src/index.js??ruleSet[1].rules[14].oneOf[10].use[3]!./src/app/globals.css ***!
  \*************************************************************************************************************************************************************************************************************************************************************************/
/*
 * Cuboid Employee App — global styles.
 *
 * IDENTITY FROM THE MARK: the logo is a fish of isometric cubes in a
 * magenta→orange spectrum on deep water. The app ground is that water; state
 * and emphasis pull from the cube spectrum. Semantic colour always pairs
 * hue with a second cue (label, pattern, weight) so done / in-progress /
 * overdue stay distinguishable without hue alone.
 */

:root {
  /* Brand — the store's own values. */
  --teal-900: #06322c;
  --teal-800: #0a5347;
  --teal-700: #108474; /* primary */
  --teal-600: #17a892;
  --teal-100: #c7ece5;
  --teal-050: #edf8f5;

  /* The primary is 4.4:1 on paper — under AA for body text. This is the
     darkened twin used wherever the brand colour becomes TEXT (6.6:1); the
     primary itself stays exact wherever it is a fill. */
  --teal-ink: #0b6558;

  /* Accent — the store's gold. Used sparingly: the entrance caption, focus
     furniture. Never as body text on paper. */
  --gold: #d8b039;
  --gold-soft: #f6e6b8;

  --ink: #0e2723;
  --ink-soft: #456b64;
  --paper: #f5faf8;
  --card: #ffffff;
  --line: #d7e8e2;
  /* Hairline for separators INSIDE a grouped card — quieter than --line. */
  --line-soft: #e8f1ee;

  --ok: #0f766e;
  --ok-bg: #ccfbf1;
  --warn: #b45309;
  --warn-bg: #fef3c7;
  --err: #b91c1c;
  --err-bg: #fee2e2;
  --info-bg: #e0f2fe;
  --info-ink: #075985;

  /* Deep-water tones. Kept as TOKENS because the landing tank and the cube
     mark are drawn in them — never as the app's paper. The app is light. */
  --water-950: #03141c;
  --water-900: #041c28;
  --water-800: #0a2f3f;
  --water-700: #124556;

  /* Cube spectrum — the mark's own gradient, used as an accent fill only. */
  --cube-magenta: #e2187c;
  --cube-violet: #b44ad9;
  --cube-coral: #ff5a3c;
  --cube-orange: #ff8a2b;
  --cube-gold: var(--gold);
  /* MEASURED against cuboidstore.com: the store uses #108474 teal (26 uses)
     and #d8b039 gold (6). Magenta and coral appear ZERO times — they were
     PR #8's invention. The accent gradient is the store's own two colours. */
  --grad-cube: linear-gradient(120deg, var(--teal-700), var(--gold));

  --radius: 14px;
  /* iOS-soft: a grouped card floats on the paper, it doesn't sit in a box. */
  --shadow: 0 1px 2px rgba(6, 50, 44, 0.05), 0 4px 16px rgba(6, 50, 44, 0.05);

  /* One tab-bar height, referenced by the bar, the content inset and the
     chat composer so the three can never disagree. */
  --tabbar-h: 54px;

  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --safe-top: env(safe-area-inset-top, 0px);

  --font-display: "Syne", "Avenir Next", sans-serif;
  --font-body: "Source Sans 3", "Segoe UI", sans-serif;
}


* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  padding: 0;
}

body {
  font-family: var(--font-body);
  background: var(--paper);
  color: var(--ink);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;
}

a {
  color: var(--cube-gold);
}

.tabular {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1, "tnum";
}

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

.stack {
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
}

.page {
  padding: 0.9rem 1rem 2rem;
  max-width: 40rem;
  margin: 0 auto;
  width: 100%;
}

/* iOS large-title rhythm: one big bold title, a small quiet subtitle only
   when it earns its place, the same top spacing on every screen. */
.page__head {
  margin: 0.35rem 0 1.1rem;
}

.page__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(1.85rem, 6vw, 2.35rem);
  font-weight: 750;
  letter-spacing: -0.03em;
  line-height: 1.1;
  background: var(--grad-cube);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.page__lede {
  margin: 0.2rem 0 0;
  color: var(--ink-soft);
  font-size: 0.9rem;
}

.card {
  background: var(--card);
  border-radius: var(--radius);
  padding: 0.95rem 1rem;
  box-shadow: var(--shadow);
}

.card--muted {
  box-shadow: none;
  background: color-mix(in srgb, var(--card) 70%, var(--paper));
}

.card__title {
  margin: 0 0 0.2rem;
  font-size: 1.02rem;
  letter-spacing: -0.01em;
}

.card__body {
  margin: 0;
  color: var(--ink-soft);
  font-size: 0.88rem;
}

/* --- inset grouped list (iOS Settings-style) ----------------------------- */

.group {
  display: block;
  background: var(--card);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  overflow: hidden;
}

.group__row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  min-height: 44px;
  padding: 0.75rem 1rem;
  text-decoration: none;
  color: var(--ink);
}

.group__row + .group__row {
  border-top: 1px solid var(--line-soft);
}

a.group__row:active {
  background: rgba(255, 255, 255, 0.06);
}

.group__main {
  flex: 1 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
}

.group__label {
  font-weight: 600;
  font-size: 1rem;
}

.group__sub {
  font-size: 0.82rem;
  color: var(--ink-soft);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.group__chevron {
  flex-shrink: 0;
  color: color-mix(in srgb, var(--ink-soft) 45%, var(--paper));
  font-size: 1.3rem;
  font-weight: 600;
  line-height: 1;
}

/* --- home dashboard cards -------------------------------------------------
 * Tappable summary cards (home/page.tsx): a .card that is ALSO a link —
 * title row with a trailing meta + chevron, then one glanceable body. Same
 * inset-card language as .group, but each card carries content, not a
 * redirect label.
 */

.dash-card {
  display: block;
  text-decoration: none;
  color: var(--ink);
}

a.dash-card:active {
  background: var(--teal-050);
}

.dash-card__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
}

.dash-card__meta {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--ink-soft);
  white-space: nowrap;
}

.dash-card__sub {
  margin: 0.25rem 0 0;
  font-size: 0.88rem;
  color: var(--ink-soft);
}

/* --- the store clock + next shift (Home's opening lines) -----------------
 * David: "everyones homepage should have a date and time thing ... their
 * shift time ... it needs to be a meaningful home page". Two quiet lines and
 * one loud one, above the cards. Deliberately NOT a card: this is the
 * screen's opening statement, and boxing it would make it compete with the
 * three summaries below instead of introducing them.
 *
 * Text keeps the page's own 1rem gutter (.page) — nothing here sets a
 * negative margin or full-bleed width, so no line ever touches the edge.
 */
.storeclock {
  margin: -0.35rem 0 1rem;
}

.storeclock__day {
  margin: 0;
  font-size: 0.82rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.storeclock__now {
  margin: 0.1rem 0 0;
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.5rem;
  /* Reserves the clock's line height before the client component mounts, so
     the cards below do not jump down a tick after hydration. */
  min-height: 2.1rem;
}

.storeclock__time {
  font-size: 1.7rem;
  font-weight: 800;
  letter-spacing: -0.02em;
  line-height: 1.15;
}

/* Only rendered when the reader's device is on a different clock. */
.storeclock__zone {
  font-size: 0.72rem;
  color: var(--ink-soft);
  opacity: 0.85;
}

.storeclock__shift {
  margin: 0.45rem 0 0;
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.4rem;
  font-size: 0.95rem;
}

.storeclock__shift-head {
  font-weight: 700;
}

.storeclock__shift-hint {
  font-size: 0.85rem;
  color: var(--ink-soft);
}

/* On shift right now: the one state worth a colour, because it is the one
   state that changes what you do next. */
.storeclock__shift--on .storeclock__shift-head {
  color: var(--teal-ink);
}

.storeclock__shift--on::before {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--teal-700);
  align-self: center;
}

/* The rotation week's real dates, under the card head. Its own line rather
   than the meta column, which already holds "Week 2 of 4" and the chevron —
   on a 375px phone the three together do not fit across. */
.dash-card__dates {
  margin: 0.15rem 0 0;
  font-size: 0.78rem;
  color: var(--ink-soft);
  opacity: 0.9;
}

/* The employee's own day: the shift range is the headline number. */
.dash-today {
  margin: 0.3rem 0 0;
  font-size: 1.15rem;
  font-weight: 700;
  letter-spacing: -0.01em;
}

.dash-today__hours {
  font-weight: 600;
  font-size: 0.9rem;
  color: var(--ink-soft);
}

.dash-today__chip {
  margin-left: 0.45rem;
  vertical-align: 0.15rem;
}

/* The management tiers' who's-in-today rail: name → today's range, hairline
   separators, on-shift rows first (home-dashboard.ts orders them). */
.dash-people {
  list-style: none;
  margin: 0.45rem 0 0;
  padding: 0;
}

.dash-people__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.6rem;
  min-height: 1.9rem;
  font-size: 0.88rem;
}

.dash-people__row + .dash-people__row {
  border-top: 1px solid var(--line-soft);
}

.dash-people__name {
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.dash-people__shift {
  flex-shrink: 0;
  color: var(--teal-ink);
  font-weight: 600;
  white-space: nowrap;
}

.dash-people__shift--out {
  color: var(--ink-soft);
  font-weight: 500;
}

/* The collapsed "everyone else" line under the who's-in rail. */
.dash-people__rest {
  margin: 0.35rem 0 0;
  font-size: 0.78rem;
  color: var(--ink-soft);
}

/* The unread count, in the brand's own notification gold (same rule as the
   chat list's dot — a small pill is exactly the accent's job). */
.dash-unread {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.3rem;
  height: 1.3rem;
  padding: 0 0.35rem;
  border-radius: 999px;
  background: var(--gold);
  color: #3d2f08;
  font-size: 0.72rem;
  font-weight: 700;
}

/* --- the demo-honesty caption: a chip everywhere, the sentence once ------
 * (external review, S11 — see src/components/DemoNote.tsx for the rule.)
 * The CHIP is always present; the long sentence appears on the session's
 * first screen and thereafter one tap behind it. Nothing is removed, only
 * disclosed at the right moment.
 */

.demo-note {
  margin: -0.4rem 0 0.9rem;
}

.demo-note__chip {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  font: inherit;
  padding: 0.25rem 0.3rem 0.25rem 0;
  min-height: 32px;
  background: none;
  border: none;
  cursor: pointer;
  color: var(--ink-soft);
}

.demo-note__hint {
  font-size: 0.78rem;
  color: var(--info-ink);
  opacity: 0.75;
}

.demo-note__text {
  margin: 0.15rem 0 0;
  font-size: 0.72rem;
  color: var(--ink-soft);
  opacity: 0.9;
}

.badge {
  display: inline-block;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  padding: 0.15rem 0.5rem;
  border-radius: 999px;
}

.badge--demo {
  background: var(--info-bg);
  color: var(--info-ink);
}

.attribution {
  font-size: 0.8rem;
  color: var(--ink-soft);
}

.attribution--on-dark {
  color: rgba(245, 250, 248, 0.78);
}

/* --- forms ---------------------------------------------------------------- */

.field {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

.field__label {
  font-weight: 600;
  font-size: 0.95rem;
}

.field__hint {
  font-size: 0.85rem;
  color: var(--ink-soft);
}

.field input,
.otp-input {
  font: inherit;
  padding: 0.7rem 0.85rem;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--card);
  color: var(--ink);
}

.field input:focus-visible {
  outline: 2px solid var(--teal-600);
  outline-offset: 1px;
}

.otp-input {
  letter-spacing: 0.45em;
  font-size: 1.3rem;
  text-align: center;
}

.btn {
  font: inherit;
  font-weight: 700;
  min-height: 44px;
  padding: 0.7rem 1.1rem;
  border-radius: 10px;
  border: none;
  background: var(--grad-cube);
  color: #1a0a12;
  cursor: pointer;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.3rem;
}

.btn:disabled {
  opacity: 0.6;
  cursor: default;
}

.btn:not(:disabled):hover {
  filter: brightness(1.05);
}

.btn--quiet {
  background: transparent;
  color: var(--cube-gold);
  filter: none;
}

.btn--quiet:not(:disabled):hover {
  background: var(--teal-050);
}

.btn--inline {
  min-height: 40px;
  padding: 0.45rem 0.7rem;
}

.actions {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
}

.notice {
  margin: 0;
  padding: 0.7rem 0.9rem;
  border-radius: 10px;
  font-size: 0.92rem;
}

.notice--error {
  background: var(--err-bg);
  color: var(--err);
}

.notice--ok {
  background: var(--ok-bg);
  color: var(--ok);
}

.notice--info {
  background: var(--info-bg);
  color: var(--info-ink);
}

.notice--compact {
  padding: 0.45rem 0.7rem;
  font-size: 0.85rem;
}

/* --- landing -------------------------------------------------------------- */

.landing {
  position: relative;
  min-height: 100dvh;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  background: linear-gradient(180deg, var(--teal-900) 0%, var(--teal-800) 55%, var(--teal-700) 100%);
}

/* A slow drifting light-band, like water surface light. Decorative only. */
.landing__water {
  position: absolute;
  inset: -20%;
  background:
    radial-gradient(60% 40% at 30% 20%, rgba(199, 236, 229, 0.16), transparent 70%),
    radial-gradient(50% 35% at 75% 70%, rgba(23, 168, 146, 0.38), transparent 70%);
  animation: landing-drift 14s ease-in-out infinite alternate;
}

@keyframes landing-drift {
  from {
    transform: translate3d(-2%, -1%, 0) scale(1);
  }
  to {
    transform: translate3d(2%, 1.5%, 0) scale(1.05);
  }
}

/* --- THE TANK, AND THE FISH THAT LIVES IN IT ------------------------------
 * Operator, 2026-08-09: "make the background water like a tank maybe a light
 * coming thru the top some plant even nothing crazy excessive but like a fish
 * tank", and "can we have the fish just swimming around randomly turning and
 * maybe doing a loop or trick".
 *
 * …AND THE WANDER THAT ANSWER PRODUCED IS GONE AGAIN (operator, 2026-08-10:
 * "the fish swimming around broke things go back to the fixed location but
 * still animated fish and keep the bubbles coming periodically"). The roaming
 * swim, the facing flip and the loop-the-loop are removed; what the ask above
 * actually wanted — a tank that is alive — is carried by everything else in
 * this section. See `.landing__anchor` for the full post-mortem on what the
 * wander cost (a mirrored logo, a squashed one, and an unpredictable origin
 * for the shatter).
 *
 * RESTRAINT IS THE BRIEF. Sol warned that a forever-rising bubble COLUMN risks
 * a screensaver (S18) — the fix for that is spacing, not silence, so the
 * bubbles rise on long out-of-phase loops rather than stopping dead after one
 * pass. So the tank is: light refracting off the surface into a slow wavering
 * net, three planted clumps rooted at the bottom swaying with the water,
 * bubbles leaving the substrate every couple of seconds, and one fish holding
 * station with a gentle bob.
 *
 * Every animated property is transform/opacity, and the mark itself is the
 * client's intact file — nudged and tilted a hair, never mirrored, never
 * scaled, never recolored.
 * The whole tank stops under prefers-reduced-motion (see the media block).
 */

/* --- CAUSTICS: the underside of a moving water surface --------------------
 * Operator, 2026-08-10: "the lights from top shouldn't be show lights but
 * like reflection of water refracting off surface".
 *
 * WHAT WAS WRONG. The previous pass drew three discrete vertical shafts —
 * `.landing__rays`, soft cones from the top edge. Those are GOD RAYS: light
 * that has already left the surface and is travelling through the water at
 * you. They have a source and a direction, so they read as stage lighting
 * pointed at the fish, and no amount of lowering their opacity fixes that —
 * the shape itself is the wrong idea. They are gone; nothing renders a beam.
 *
 * WHAT REPLACES IT. Caustics — the wavering net a rippling surface focuses
 * onto everything beneath it. The physical thing has no edges and no source
 * you can point at; it is a moving interference pattern. So each layer is TWO
 * repeating gradients crossed at different angles: where two bright bands
 * overlap you get a bright cell, and where they miss you get water. Three
 * such layers, each blurred and drifting/deforming on its own long period,
 * slide across one another — so the bright cells are constantly being made
 * and unmade, which is the shimmer, and no single layer is ever legible as a
 * pattern. `screen` blending makes the overlaps ADD like light rather than
 * stack like paint.
 *
 * IT MUST BE THE SECOND THING YOU NOTICE. Peak opacity is ~0.6 of already
 * translucent bands, the periods are 19-31s, and a mask fades the whole thing
 * out with depth — the surface is above you, so its light belongs at the top.
 * Cheap: every animated property is transform or opacity, and the blur is on
 * a static background so it is rasterised once.
 */
.landing__caustics {
  position: absolute;
  inset: 0 0 auto;
  height: 62%;
  overflow: hidden;
  pointer-events: none;
  z-index: 0;
  /* Depth fade — brightest just under the surface, gone by mid-tank. */
  -webkit-mask-image: linear-gradient(
    to bottom,
    rgba(0, 0, 0, 1) 0%,
    rgba(0, 0, 0, 0.72) 34%,
    rgba(0, 0, 0, 0.22) 72%,
    transparent 100%
  );
  mask-image: linear-gradient(
    to bottom,
    rgba(0, 0, 0, 1) 0%,
    rgba(0, 0, 0, 0.72) 34%,
    rgba(0, 0, 0, 0.22) 72%,
    transparent 100%
  );
}

.landing__caustics span {
  position: absolute;
  /* Oversized so a drifting layer never exposes its own edge. */
  inset: -45% -35%;
  filter: blur(9px);
  mix-blend-mode: screen;
  will-change: transform, opacity;
  animation: landing-caustic 24s ease-in-out infinite;
}

/* THE BANDS RUN WITH THE SURFACE, NOT DOWN FROM IT.
   Every gradient angle here is within ~20 degrees of vertical, which puts the
   BANDS (perpendicular to the gradient's direction) within ~20 degrees of
   HORIZONTAL. That is not a detail: the first attempt at this used angles
   near 90deg, which produces near-vertical stripes — the exact silhouette of
   the god rays being replaced, just softer. Caustics lie roughly parallel to
   the surface that casts them and undulate along it.

   Each layer crosses its own two band sets at slightly different angles, so
   the three interference patterns never coincide; a shallow crossing angle is
   what makes the bright overlaps long lens-shaped ribbons rather than a
   checkerboard. */
.landing__caustics span:nth-child(1) {
  background:
    repeating-linear-gradient(
      171deg,
      transparent 0 2.6%,
      rgba(214, 245, 238, 0.1) 3.6%,
      rgba(214, 245, 238, 0.2) 4.2%,
      rgba(214, 245, 238, 0.08) 5%,
      transparent 6.4% 9.5%
    ),
    repeating-linear-gradient(
      158deg,
      transparent 0 3.4%,
      rgba(199, 236, 229, 0.07) 4.6%,
      rgba(199, 236, 229, 0.15) 5.3%,
      transparent 7% 11.5%
    );
  filter: blur(8px);
  animation-duration: 24s;
}

.landing__caustics span:nth-child(2) {
  background:
    repeating-linear-gradient(
      186deg,
      transparent 0 4%,
      rgba(214, 245, 238, 0.08) 5.2%,
      rgba(214, 245, 238, 0.16) 6%,
      transparent 7.8% 13%
    ),
    repeating-linear-gradient(
      199deg,
      transparent 0 3%,
      rgba(237, 248, 245, 0.06) 4.2%,
      rgba(237, 248, 245, 0.13) 4.8%,
      transparent 6.2% 9.5%
    );
  animation-duration: 31s;
  animation-delay: -9s;
  animation-direction: reverse;
}

.landing__caustics span:nth-child(3) {
  background:
    repeating-linear-gradient(
      165deg,
      transparent 0 5%,
      rgba(199, 236, 229, 0.07) 6.4%,
      rgba(199, 236, 229, 0.14) 7.4%,
      transparent 9.4% 17%
    );
  filter: blur(15px);
  animation-duration: 19s;
  animation-delay: -13s;
}

/* A closed loop rather than `alternate`: water does not retrace its own
   ripple backwards. Translate + rotate + a breathing scale, so the crossed
   bands shear against the other layers instead of sliding rigidly. */
@keyframes landing-caustic {
  0% { transform: translate3d(-3%, -2.5%, 0) rotate(-1.1deg) scale(1.06); opacity: 0.4; }
  28% { transform: translate3d(1.4%, 0.8%, 0) rotate(0.5deg) scale(1.13); opacity: 0.62; }
  53% { transform: translate3d(3.2%, -1.4%, 0) rotate(1.3deg) scale(1.05); opacity: 0.47; }
  79% { transform: translate3d(0.6%, 1.6%, 0) rotate(-0.4deg) scale(1.1); opacity: 0.58; }
  100% { transform: translate3d(-3%, -2.5%, 0) rotate(-1.1deg) scale(1.06); opacity: 0.4; }
}

/* Plants, rooted at the bottom edge. Silhouettes, so they read as depth.
 *
 * ONE SVG PER CLUMP, SIZED BY HEIGHT. The first pass put every plant in a
 * single full-width viewBox with `preserveAspectRatio="none"`, so the leaves
 * were stretched by whatever the viewport's aspect happened to be — which is
 * how they ended up reading as abstract ribbons rather than plants. Each clump
 * is now its own SVG placed by CSS and sized by HEIGHT with `width: auto`, so
 * its own aspect ratio is preserved at every screen size. The leaves are
 * FILLED tapering blades (see page.tsx), which is what makes a leaf silhouette
 * — a constant-width stroke cannot taper to a point. */
.landing__plants {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 26vh;
  max-height: 190px;
  pointer-events: none;
  z-index: 0;
}

/* THE PLANTS MOVE WITH THE WATER (operator, 2026-08-10: the clumps read as
 * "static silhouettes"). They were technically animated — a ±2.2° rotate on a
 * 13s alternate — but a rigid rotation of an 84%-tall clump moves its tips
 * about seven pixels, which at that speed is indistinguishable from still.
 * Two things were wrong with it beyond the amplitude:
 *
 *   - A PURE ROTATION MOVES THE WHOLE CLUMP AS A BOARD. Rooted leaves do not
 *     do that: the base is anchored in the substrate and the tips travel. The
 *     sway is now `rotate() skewX()` about a `50% 100%` origin — the rotate
 *     supplies the lean, the SHEAR adds displacement proportional to height,
 *     so motion is near-zero at the root and greatest at the tips. (A skew is
 *     not a mirror: its determinant is 1 and it never inverts an axis — see
 *     the no-mirror invariant in tests/landing-motion.test.mjs.)
 *   - THEY MOVED IN UNISON. Same keyframe, same shape of motion, only the
 *     duration differed. Each clump now carries its own amplitudes as
 *     variables as well as its own period and phase, so no two are ever at
 *     the same pose, and the loop itself is four uneven stops rather than a
 *     metronome from A to B.
 *
 * Still slow (13-21s), still low-contrast, still ambient — water, not wind. */
.landing__weed {
  position: absolute;
  /* Rooted just past the edge so no clump floats above the substrate. */
  bottom: -1px;
  width: auto;
  /* `fill` is an inherited SVG presentation property, so the blades take
     this without each path restating it. */
  fill: #04241f;
  opacity: 0.5;
  /* The root. Everything the sway does is measured from here, which is what
     makes the base still and the tips travel. */
  transform-origin: 50% 100%;
  /* Measured, not guessed: at these amplitudes a clump's tips travel about a
     tenth of its own height end to end (~19px on the tallest), which is
     plainly moving when you watch it and invisible when you are not. The
     ±2.2deg rigid rotate this replaced moved them ~6px and the operator read
     the result as a still image. */
  --sway-rot: 4.4deg;
  --sway-skew: 3.6deg;
  will-change: transform;
  animation: landing-sway 15s ease-in-out infinite;
}

/* Placed, sized AND tuned individually — a row of clumps sharing one motion
   is wallpaper however slowly it moves. */
.landing__weed--a {
  left: 5%;
  height: 84%;
  animation-duration: 15s;
  animation-delay: -3s;
}

.landing__weed--b {
  left: 40%;
  height: 100%;
  --sway-rot: 3.5deg;
  --sway-skew: 4.6deg;
  animation-duration: 21s;
  animation-delay: -11s;
}

.landing__weed--c {
  right: 7%;
  height: 72%;
  --sway-rot: 5.3deg;
  --sway-skew: 2.7deg;
  animation-duration: 13s;
  animation-delay: -7s;
}

/* A closed, uneven loop — the lean and the shear peak at different moments,
   so a clump bends rather than tips. */
@keyframes landing-sway {
  0% {
    transform: rotate(calc(var(--sway-rot) * -1)) skewX(calc(var(--sway-skew) * -0.55));
  }
  27% {
    transform: rotate(calc(var(--sway-rot) * 0.25)) skewX(var(--sway-skew));
  }
  56% {
    transform: rotate(var(--sway-rot)) skewX(calc(var(--sway-skew) * 0.3));
  }
  81% {
    transform: rotate(calc(var(--sway-rot) * -0.3)) skewX(calc(var(--sway-skew) * -1));
  }
  100% {
    transform: rotate(calc(var(--sway-rot) * -1)) skewX(calc(var(--sway-skew) * -0.55));
  }
}

/* The swimming layer: absolutely positioned so the fish can roam without
   moving the title, which stays put in normal flow underneath. */
.landing__tank {
  position: absolute;
  inset: 0;
  z-index: 1;
}

/* THE STAGE, AND THE ONE CLOCK EVERYTHING ON IT MOVES BY.
 *
 * The panel rising and the fish lifting are ONE gesture (operator, 2026-08-10:
 * "shift the component to the center and move the fish up at the same time"),
 * so they share these numbers rather than each carrying their own. Two
 * animations with almost-equal durations read as two animations fighting; the
 * way to make that impossible is to give them nothing to disagree about. They
 * are also triggered by the same class on this same element, so they start in
 * the same frame as well as ending in it. */
.landing__stage {
  position: absolute;
  inset: 0;
  --gate-move-ms: 420ms;
  --gate-move-ease: cubic-bezier(0.2, 0.72, 0.24, 1);
  /* How far the fish rises to make room for the panel. Capped in px so a tall
     desktop viewport does not fling it off the top. */
  --gate-lift: min(19vh, 210px);
}

/* On a short viewport — a phone with the keyboard up — there is nowhere to
   lift TO, and the form is the only thing that matters. Barely move. */
@media (max-height: 620px) {
  .landing__stage {
    --gate-lift: min(7vh, 42px);
  }
}

/* THE FISH HAS ONE BERTH, AND IT STAYS IN IT.
 *
 * WHAT THE WANDER COST (operator, 2026-08-10: "the fish swimming around broke
 * things go back to the fixed location but still animated fish"). The previous
 * build gave the mark four nested loops — a ±22vw horizontal wander, a ±7vh
 * vertical one, a facing flip and a 47s loop-the-loop. Measured on the running
 * page at 375x812 it never left the tank and never reached the title, so the
 * damage was not layout. It was the MARK:
 *
 *   - THE FACING FLIP MIRRORED THE CLIENT'S LOGO. `scaleX(-1)` held for half
 *     of every 29s cycle, so for ~15 seconds at a time the fish — an actual
 *     brand mark, with an eye — was drawn backwards.
 *   - THE TURN SQUASHED IT. Passing through `scaleX(0.12)` to sell the turn
 *     took the mark from 173px wide to 44px and back. On a real fish that
 *     reads as turning edge-on; on a logo it reads as the image collapsing.
 *     ("moved and mirrored, never distorted" was the claim; a 0.12 scale is a
 *     distortion.)
 *   - IT MADE THE SHATTER'S ORIGIN UNPREDICTABLE. The cubes fly from wherever
 *     the fish had drifted to — which could be mirrored, or mid-squash — while
 *     the sign-in reassembly always builds at ITS page's own fixed centre. The
 *     two halves of one movement started and ended in different places, so the
 *     arc read as two effects rather than one.
 *
 * So: a fixed anchor that never moves, and a gentle idle inside it. The idle
 * is the pre-wander loop restored — a couple of percent of drift, ~2° of tilt,
 * 11 seconds, no mirroring and no scaling anywhere in it. The mark is only
 * ever translated and rotated a hair; it is never flipped and never squashed.
 */
.landing__anchor {
  position: absolute;
  left: 50%;
  top: 34%;
  width: min(46vmin, 210px);
  margin-left: calc(min(46vmin, 210px) / -2);
  margin-top: calc(min(46vmin, 210px) * -0.5 * 544 / 862);
  z-index: 2;
}

.landing__idle {
  animation: landing-idle 11s ease-in-out infinite;
  will-change: transform;
}

/* Aquarium, not arcade: a couple percent of drift, ~2° of tilt, 11s. */
@keyframes landing-idle {
  0% { transform: translate3d(-2.5%, 0, 0) rotate(0deg); }
  25% { transform: translate3d(0.5%, -2.2%, 0) rotate(-2.1deg); }
  50% { transform: translate3d(2.5%, 0.8%, 0) rotate(0.5deg); }
  75% { transform: translate3d(0.5%, 1.8%, 0) rotate(1.9deg); }
  100% { transform: translate3d(-2.5%, 0, 0) rotate(0deg); }
}

/* The real mark is a full-colour PNG — sized by width, never tinted, and
   never given a fixed height (862:544 is not 8:5, and forcing one squashes
   the client's own logo by a pixel or two). */
.landing__fish {
  position: relative;
  width: 100%;
  aspect-ratio: 862 / 544;
}

.landing__fish--still {
  width: min(46vmin, 210px);
  margin: 0 auto;
}

.landing__logo {
  display: block;
  width: 100%;
  height: auto;
}

.landing__bubbles {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 1;
}

.landing__bubble {
  position: absolute;
  bottom: 8%;
  aspect-ratio: 1;
  border-radius: 50%;
  opacity: 0;
  background:
    radial-gradient(circle at 34% 30%, rgba(255, 255, 255, 0.9) 0 12%,
      rgba(199, 236, 229, 0.3) 42%, rgba(199, 236, 229, 0.05) 72%);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.3);
  will-change: transform, opacity;
  /* BUBBLES KEEP COMING, PERIODICALLY (operator, 2026-08-10: "keep the bubbles
     coming periodically"). The build before this one released each bubble
     exactly ONCE — `1 forwards`, all six done inside ~2.4s — so the tank was
     lifeless a moment after it loaded.
     They loop again, but SLOWLY and out of phase: six lanes, 9.5-14s each,
     mutually prime-ish durations and staggered delays, which works out to a
     bubble leaving the substrate every ~2s somewhere on screen and no visible
     repeat for minutes. That is the middle ground Sol's S18 was pointing at —
     the failure mode there was a dense column that turns the page into a
     screensaver, not the existence of a second bubble. Each one fades in low
     and fades out before the top, so the loop's restart is never seen. */
  animation: landing-bubble 11s cubic-bezier(0.35, 0.1, 0.5, 1) infinite;
  z-index: 1;
}

.landing__bubble--front { z-index: 3; }

.landing__bubble:nth-child(1) { left: 16%; width: 2.4vmin; animation-duration: 11s; animation-delay: 0s; --bubble-dx: 3vmin; }
.landing__bubble:nth-child(2) { left: 40%; width: 1.6vmin; animation-duration: 13.5s; animation-delay: -2.2s; --bubble-dx: -2.4vmin; }
.landing__bubble:nth-child(3) { left: 62%; width: 3vmin; animation-duration: 9.5s; animation-delay: -4.1s; --bubble-dx: 4vmin; }
.landing__bubble:nth-child(4) { left: 78%; width: 1.8vmin; animation-duration: 14s; animation-delay: -1.3s; --bubble-dx: -3.2vmin; }
.landing__bubble:nth-child(5) { left: 29%; width: 2.2vmin; animation-duration: 12s; animation-delay: -6.4s; --bubble-dx: 2.2vmin; }
.landing__bubble:nth-child(6) { left: 52%; width: 1.4vmin; animation-duration: 10.5s; animation-delay: -3.2s; --bubble-dx: -1.8vmin; }

@keyframes landing-bubble {
  0% { opacity: 0; transform: translate3d(0, 10%, 0) scale(0.55); }
  10% { opacity: 0.7; }
  70% { opacity: 0.42; }
  100% { opacity: 0; transform: translate3d(var(--bubble-dx, 2vmin), -60vh, 0) scale(1.05); }
}

.landing__content {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 22%;
  z-index: 2;
  padding: 1.5rem;
  text-align: center;
  pointer-events: none;
}

.landing__content--locked {
  position: relative;
  bottom: auto;
  pointer-events: auto;
}

/* --- THE WORDMARK ---------------------------------------------------------
 * Operator, 2026-08-10: "make cuboid nature aquarium stacked like 'cuboid'
 * 'nature Aquarium' and more title like font".
 *
 * It was a 1.4rem/700 line of the body face — a name set as a sentence. A
 * NAME IS NOT A SENTENCE: the store's own short name gets the size and the
 * weight, and the qualifier goes underneath it small, uppercase and widely
 * tracked. That size/weight/tracking contrast between two stacked lines is
 * the whole of what makes type read as a logotype rather than a caption; it
 * is a relationship, not a typeface, which is why this needs no new font.
 *
 * WHY NO DISPLAY WEBFONT. The app's CSP admits no external font host and the
 * repo bundles no face, so a display font would be either a policy hole or a
 * ~60kB download for two words on the one screen that must paint instantly.
 * The presence is carried by weight (800 vs 600), scale (~4x), case, tracking
 * (-0.035em against +0.3em) and a 0.94 line-height, on the same system UI
 * stack as the rest of the app.
 *
 * IT FITS 375px. The lead is `clamp`ed and sized in vw between those stops,
 * so "Cuboid" never wraps and never runs into the fish above (which is
 * anchored in the top third) or the sign-in line below.
 */
.landing__name {
  margin: 0;
  color: var(--paper);
  text-shadow: 0 2px 18px rgba(4, 36, 31, 0.6);
}

.landing__name-lead {
  display: block;
  font-size: clamp(2.3rem, 12.4vw, 3.35rem);
  font-weight: 800;
  line-height: 0.94;
  /* Big type needs LESS tracking, not the default — a logotype is tight. */
  letter-spacing: -0.035em;
}

.landing__name-rest {
  display: block;
  margin-top: 0.55rem;
  font-size: clamp(0.7rem, 3.05vw, 0.85rem);
  font-weight: 600;
  line-height: 1.15;
  letter-spacing: 0.3em;
  /* Letter-spacing adds its gap AFTER the last character too, which drags a
     centred line half a space to the left. The matching indent puts it back
     on the optical centre. */
  text-indent: 0.3em;
  text-transform: uppercase;
  color: var(--gold-soft);
}

.landing__enter {
  display: block;
  margin-top: 1.15rem;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--paper);
  opacity: 0.72;
  letter-spacing: 0.04em;
}

.landing__hit {
  position: absolute;
  inset: 0;
  z-index: 4;
  text-indent: -9999px;
  overflow: hidden;
}

/* --- THE SIGN-IN PANEL: the form comes to the landing ---------------------
 * Operator, 2026-08-10: "maybe the click anywhere would render the login to
 * fade up onto the landing page there instead of having a dedicated one? and
 * so when they click the login option then the main fish breaks apart".
 *
 * The tap used to shatter the mark and navigate to /signin, where a second
 * fish at a different size in a different document had to be reconciled with
 * the first. The panel removes that hop entirely: it RISES onto the landing
 * while the tank carries on behind it — the fish still breathing in its
 * berth, the caustics still moving, the plants still swaying, the bubbles
 * still rising — and the fish that breaks apart on submit is that same fish,
 * in the same box, in the same document. There is nothing left to hand over.
 *
 * WHY THIS IS NOT A `<dialog>`. Modal `<dialog>` promotes its contents to the
 * top layer with its own backdrop, which would put the panel ABOVE the tank
 * rather than on it — the living page behind the sheet is the entire point.
 * The three behaviours a modal owes (focus in on open, Escape and outside-tap
 * to close, Tab trapped within) are implemented in LandingGate instead, and
 * the sheet still carries `role="dialog"` and `aria-modal`.
 */
/* --- ONE GESTURE: the panel rises AS the fish lifts ------------------------
 * Operator, 2026-08-10: "can we somehow shift the component to the center and
 * move the fish up at the same time? dont want to have it just align to the
 * bottom like that".
 *
 * The first pass pinned the sheet to the bottom edge and left the fish
 * floating in whatever space was left over — two objects sharing a screen
 * rather than a composition. Now the two move TOGETHER into a balanced one:
 * the fish lifts into the upper third, the panel settles in the middle with
 * water above it and below it, and neither touches an edge.
 *
 * THEY CANNOT DRIFT APART, BECAUSE THEY SHARE THESE NUMBERS. Duration and
 * easing live on the stage as custom properties and every part of the
 * recomposition reads them; the lift and the rise are triggered by the SAME
 * class on the SAME element, so they also start in the same frame. Two
 * animations with almost-equal durations read as two animations fighting, and
 * the way to make that impossible is to give them nothing to disagree about.
 */
.landing__stage--panel .landing__anchor {
  animation: gate-lift var(--gate-move-ms) var(--gate-move-ease) both;
}

/* The reverse, for BOTH exits: a dismissal and a submit run the identical
   motion — the submit simply carries on into the break-up afterwards. */
.landing__stage--closing .landing__anchor {
  animation: gate-drop var(--gate-move-ms) var(--gate-move-ease) both;
}

/* TRANSLATE ONLY. The mark does not shrink to make room and does not change
   its facing; it moves. (A scale here would also have to answer to the
   no-mirror gate — see tests/landing-motion.test.mjs.) */
@keyframes gate-lift {
  from { transform: translate3d(0, 0, 0); }
  to { transform: translate3d(0, calc(-1 * var(--gate-lift)), 0); }
}

@keyframes gate-drop {
  from { transform: translate3d(0, calc(-1 * var(--gate-lift)), 0); }
  to { transform: translate3d(0, 0, 0); }
}

/**
 * THE SCROLL CONTAINER — and the short-viewport trap it is shaped to avoid.
 *
 * `align-items: center` on a scrollable flex container CLIPS its child at the
 * top the moment the child is taller than the box, and content that overflows
 * ABOVE a scroll container cannot be scrolled to: it is simply gone. On a
 * phone with the keyboard open — where the code step is the tall one — that
 * would put the first field out of reach. `margin: auto` on the sheet centres
 * it when there is room and degrades to ordinary scrollable flow when there is
 * not, which is the whole reason the /signin wrapper is built the same way.
 */
.landing-panel {
  position: absolute;
  inset: 0;
  z-index: 6;
  display: flex;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: 1rem;
  padding-bottom: calc(1rem + var(--safe-bottom));
}

/* Deepens the water behind the sheet so the form has contrast to sit on,
   without hiding the tank. A SIBLING of the scroll container, not a child:
   an absolutely-positioned child would scroll away with the content. */
.landing-panel__scrim {
  position: absolute;
  inset: 0;
  z-index: 5;
  background: radial-gradient(
      120% 60% at 50% 62%,
      rgba(4, 36, 31, 0.55),
      transparent 72%
    ),
    linear-gradient(180deg, transparent 0%, rgba(4, 36, 31, 0.35) 100%);
}

.landing-panel__sheet {
  position: relative;
  /* THE CENTRING. Not `align-items`, deliberately — see above. */
  margin: auto;
  width: 100%;
  max-width: 24rem;
  /* `flex-shrink: 0` so a tall sheet keeps its height and the CONTAINER
     scrolls, rather than the sheet being squashed into an unreadable box. */
  flex-shrink: 0;
  background: var(--card);
  border-radius: 20px;
  padding: 1.5rem 1.2rem 1.2rem;
  box-shadow: 0 14px 48px rgba(4, 36, 31, 0.45);
  outline: none;
}

.landing-panel__close {
  position: absolute;
  top: 0.35rem;
  right: 0.45rem;
  width: 2.4rem;
  height: 2.4rem;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--ink-soft);
  font-size: 1.5rem;
  line-height: 1;
  cursor: pointer;
}

.landing-panel__close:hover {
  background: var(--teal-050);
}

/* IT FADES UP — rises and fades together, on the fish's own clock. */
.landing__stage--panel .landing-panel__sheet {
  animation: panel-rise var(--gate-move-ms) var(--gate-move-ease) both;
}

.landing__stage--panel .landing-panel__scrim {
  animation: panel-scrim var(--gate-move-ms) var(--gate-move-ease) both;
}

/* AND IT LEAVES ON THE SAME CLOCK. Identical duration and easing to the
   fish's drop, started by the same class in the same frame — but the sheet is
   GONE by 70% of it, so on a submit the panel has cleared well before the
   fish reaches its berth and long before the break-up. */
.landing__stage--closing .landing-panel__sheet {
  animation: panel-fall var(--gate-move-ms) var(--gate-move-ease) both;
}

.landing__stage--closing .landing-panel__scrim {
  animation: panel-fall var(--gate-move-ms) var(--gate-move-ease) both;
}

@keyframes panel-rise {
  from { opacity: 0; transform: translate3d(0, 2.4rem, 0); }
  to { opacity: 1; transform: translate3d(0, 0, 0); }
}

/* GONE BY 55%, not 70%: at a 420ms gesture that is fully transparent by
   ~230ms, which leaves ~190ms of clear tank before the first cube flies. The
   panel leaving and the fish coming apart must read as two beats in sequence,
   not one on top of the other. */
@keyframes panel-fall {
  0% { opacity: 1; transform: translate3d(0, 0, 0); }
  55% { opacity: 0; transform: translate3d(0, 1.4rem, 0); }
  100% { opacity: 0; transform: translate3d(0, 1.4rem, 0); }
}

@keyframes panel-scrim {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* The wordmark and the "tap anywhere" line step back while the panel is up —
   the caption has been answered, and the sheet is what to read now. They come
   back with the same motion on a dismissal. */
.landing__stage--panel .landing__content {
  animation: landing-page-out var(--gate-move-ms) var(--gate-move-ease) both;
}

/* HELD AT ZERO THROUGH THE SUBMIT PATH — never re-faded from 1.
 *
 * A submit is also a `--closing`, and this rule used to be absent for it. That
 * is not "stays hidden": with no rule, the animation is REMOVED and opacity
 * returns to its natural 1, so the wordmark and the "tap anywhere" line came
 * back at the dismissal and were then re-faded from 1 by the shatter. The
 * operator saw both showing through the break-up. `animation: none` with a
 * static `opacity: 0` cannot restart, because there is nothing to restart. */
.landing__stage--closing .landing__content,
.landing__stage--depart .landing__content {
  animation: none;
  opacity: 0;
}

/* ONLY a dismissal brings it back. Source-ordered after the rule above so it
   wins on equal specificity when both classes are present. */
.landing__stage--dismissing .landing__content {
  animation: landing-page-in var(--gate-move-ms) var(--gate-move-ease) both;
}

@keyframes landing-page-in {
  from { opacity: 0; transform: translate3d(0, 1.6rem, 0); }
  to { opacity: 1; transform: translate3d(0, 0, 0); }
}

/* --- THE DEPARTURE: the fish swimming off on the way in -------------------
 * Beat 2 of the fish arc.
 *
 * IT USED TO BLOW UP HERE. The mark broke into its own hexagonal cubes and
 * they flew off frame. The client watched that on every single sign-in and
 * said: it feels like we are killing the fish. He is right, and it matters
 * more than it sounds — this is the last thing someone sees before their own
 * workplace opens, and it was reading as destruction. The cube break-up is a
 * genuinely nice piece of craft and it is still what BUILDS the mark on first
 * load (fish-tile-in), where flying pieces mean assembly. Aimed outward, the
 * same gesture means the opposite thing.
 *
 * So the fish leaves the way a fish leaves: it darts.
 *
 *   0 → windUp    a short recoil AGAINST the exit, nose dipping — the load
 *                 before a dart. Without it the mark just slides off, which
 *                 reads as a card being dismissed rather than something alive
 *                 deciding to go.
 *   → swim        out through the right-hand edge, accelerating (ease-IN, not
 *                 out: a fish does not coast to a halt on its way somewhere),
 *                 shrinking slightly for depth, holding full opacity until
 *                 the last third so it never reads as a fade-out.
 *
 * RIGHT IS FORWARD, AND THAT IS MEASURED. The eye sits at x=592 of 862, so
 * the head is on the right (EYE in src/lib/logo.ts) and the assembly stagger
 * runs tail→head as `cellPhase` increases left→right. Swimming it left would
 * be the "fish travels backwards" bug the operator already caught once — and
 * it cannot be fixed with a flip afterwards, because the mark has a canonical
 * facing and `scaleX(-1)` is banned outright across this stylesheet and every
 * component (tests/landing-motion.test.mjs enforces both).
 *
 * THE WINDOW IS DELIBERATELY THE ONE THE BREAK-UP HAD. DEPART.windUpMs +
 * DEPART.swimMs === flightSettledMs(), because the navigation race is
 * arbitrated against that bound and this change is meant to swap the gesture
 * without re-opening a timing question that took three rounds to settle.
 */

/* The idle FREEZES on submit so the dart starts from the exact pose on
   screen, rather than snapping back to a start pose first. */
.landing__stage--depart .landing__idle {
  animation-play-state: paused;
}

/* The mark itself leaves. On `.landing__fish` — the innermost of the three
   nested elements — because `.landing__anchor` owns the lift and
   `.landing__idle` owns the breath, and a transform written on either of
   those would fight a gesture that is still settling. Three transforms on
   three elements compose; two on one element do not.

   `both` holds the final frame: if the swim finishes before auth returns, the
   fish must STAY gone rather than snapping back into view for the last few
   hundred milliseconds — the same fill-mode contract the cubes relied on. */
.landing__stage--depart .landing__fish {
  animation:
    landing-fish-windup 90ms cubic-bezier(0.4, 0.0, 0.7, 0.2) both,
    landing-fish-swim 370ms cubic-bezier(0.55, 0, 0.85, 0.35) 90ms both;
  will-change: transform, opacity;
}

@keyframes landing-fish-windup {
  from {
    transform: translate3d(0, 0, 0) rotate(0deg);
  }
  to {
    /* Back and a touch down — a tail loading, not a bounce. */
    transform: translate3d(-2.4vmin, 0.6vmin, 0) rotate(-2.5deg);
  }
}

@keyframes landing-fish-swim {
  from {
    transform: translate3d(-2.4vmin, 0.6vmin, 0) rotate(-2.5deg) scale(1);
    opacity: 1;
  }
  55% {
    /* Levelling out as it picks up speed, still fully opaque — this must read
       as leaving, never as dissolving. */
    transform: translate3d(38vmax, -1.4vmin, 0) rotate(1.5deg) scale(0.94);
    opacity: 1;
  }
  to {
    /* Clear of the edge from a centred berth, with room to spare. vmax rather
       than vw so a landscape phone does not leave the tail on screen. */
    transform: translate3d(120vmax, -3vmin, 0) rotate(2.5deg) scale(0.86);
    opacity: 0;
  }
}

/* --- MOUNTED IS NOT THE SAME AS VISIBLE --------------------------------
 * Operator, 2026-08-10: "yes it played but it also flashes the login modal
 * after it dismisses after clicking an option so it covers the animation".
 * Their screenshot shows the cubes flying with the persona cards, the DEMO
 * chip and the close X all readable through a half-transparent sheet.
 *
 * THE PANEL MUST STAY MOUNTED — the form inside it is the only thing that can
 * report auth success, and unmounting it strands a slow sign-in on a finished
 * break-up forever (LandingGate). But it had no business staying VISIBLE, and
 * the previous rule here made that worse rather than better: it ran
 * `landing-page-out`, a DIFFERENT animation from the `panel-fall` that had
 * just finished, so at the class change the panel RESTARTED FROM OPACITY 1
 * and faded again over 380ms — directly on top of the cubes. A fade is the
 * wrong tool entirely; by this point the panel has already gone.
 *
 * So: no animation to restart, and no pixels. It is a pure signal carrier
 * from here — `visibility: hidden` takes it out of the accessibility tree and
 * out of the focus order in one move, which is the part `opacity: 0` alone
 * would have got wrong (a transparent element that still takes Tab is its own
 * bug). React drops its `tabindex` and marks it `aria-hidden` as well. */
.landing__stage--depart .landing-panel,
.landing__stage--depart .landing-panel__scrim,
.landing__stage--depart .landing-panel__sheet {
  animation: none;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}

@keyframes landing-page-out {
  from { opacity: 1; transform: translate3d(0, 0, 0); }
  to { opacity: 0; transform: translate3d(0, 1.6rem, 0); }
}

/* The tank's own chrome goes with it. `:has()` rather than a state class on
   <main>, because the landing is a Server Component and the shatter is client
   state; a browser without `:has()` simply keeps them, a graceful floor. */
.landing:has(.landing__stage--depart) .landing__caustics,
.landing:has(.landing__stage--depart) .landing__plants {
  animation: landing-page-out 320ms ease-in both;
}

/* The bubbles leave too. They loop forever now, so without this they would go
   on rising through the break-up — the one part of the page still behaving as
   though nothing had happened. Matched from `.landing` rather than from the
   stage: `.landing__bubbles` is the stage's PRECEDING sibling, so neither a
   descendant selector nor `~` can reach it from there. */
.landing:has(.landing__stage--depart) .landing__bubbles {
  animation: landing-page-out 300ms ease-in both;
}

.landing:has(.landing__stage--depart) .landing__footer {
  animation: landing-page-out 300ms ease-in both;
}

.landing:has(.landing__stage--depart) .landing__water {
  animation: landing-water-out 460ms ease-in both;
}

@keyframes landing-water-out {
  from { opacity: 1; }
  to { opacity: 0.25; }
}

.landing__locked {
  position: relative;
  z-index: 2;
  margin: 1rem 1.5rem;
  max-width: 26rem;
  background: var(--err-bg);
  color: var(--err);
  border-radius: var(--radius);
  padding: 1rem 1.2rem;
}

.landing__footer {
  position: absolute;
  bottom: calc(0.9rem + var(--safe-bottom));
  left: 0;
  right: 0;
  text-align: center;
  z-index: 2;
}

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

.signin-wrap {
  min-height: 100dvh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 2rem 1rem;
}

.sheet {
  width: 100%;
  max-width: 24rem;
  background: var(--card);
  border-radius: 20px;
  padding: 1.8rem 1.4rem 1.5rem;
  box-shadow: var(--shadow);
}

.signin-head {
  text-align: center;
  margin-bottom: 1.25rem;
}

.signin-logo {
  width: 118px;
  height: auto;
}

/* --- THE HANDOFF'S SOURCE SIDE -------------------------------------------
 * `.arc-source` wraps the sign-in page inside SignInArcProvider purely so
 * this selector exists. `display: contents` means it generates no box at all,
 * so it cannot shift the layout the handoff has just measured.
 *
 * `visibility: hidden`, NOT `display: none` or `opacity: 0`:
 *   - `display: none` would collapse the logo's box and reflow the sheet
 *     underneath the overlay, moving the very geometry the transition is
 *     built on.
 *   - `opacity` is animatable and invites exactly the crossfade this change
 *     exists to remove ("never a fade between them, never both visible").
 * Visibility flips in one commit, in the same render that mounts the overlay
 * fish at this element's measured box — so there is one paint, one fish. */
.arc-source {
  display: contents;
}

.arc-source--handed-off [data-shared-mark] {
  visibility: hidden;
}

/* --- demo entrance (dev builds only) -------------------------------------- */
/* Rendered only when the dev-login door is open (see signin/page.tsx). It sits
   ABOVE the real form and is captioned DEMO, so nobody mistakes a shortcut for
   the sign-in their staff will actually use. */

.personas {
  margin: 0 0 1.15rem;
  padding: 0 0 1.15rem;
  border-bottom: 1px solid var(--line-soft);
}

.personas__lede {
  margin: 0 0 0.6rem;
  font-size: 0.78rem;
  color: var(--ink-soft);
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex-wrap: wrap;
}

/* FULL-WIDTH ROWS, NOT THREE CRAMPED COLUMNS (external review, S7). Three
   68px cards on a 375px screen had no room for the one thing that matters
   before you tap — who you are about to be — so they said the role and
   nothing else. Each persona now gets a 60px row: the person's own name, a
   one-line access summary, and the tier on the right. */
.personas__rows {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.persona {
  font: inherit;
  text-align: left;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  width: 100%;
  min-height: 60px;
  padding: 0.6rem 0.85rem;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--card);
  color: var(--ink);
  cursor: pointer;
  transition: border-color 120ms ease, background 120ms ease;
}

.persona:not(:disabled):hover {
  border-color: var(--teal-600);
  background: var(--teal-050);
}

.persona:disabled {
  opacity: 0.55;
  cursor: default;
}

.persona__main {
  flex: 1 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
}

.persona__name {
  font-weight: 700;
  font-size: 1rem;
  color: var(--teal-ink);
}

.persona__access {
  font-size: 0.76rem;
  color: var(--ink-soft);
  line-height: 1.3;
}

.persona__role {
  flex-shrink: 0;
  align-self: flex-start;
  margin-top: 0.1rem;
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

/* The folded-away phone path: one quiet centered line under the persona
   cards (PhoneToggle.tsx). A text button, not a .btn — it must read as an
   aside, never compete with the cards. */
.signin-alt {
  margin: 0;
  text-align: center;
}

.signin-alt__button {
  font: inherit;
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--teal-ink);
  background: transparent;
  border: none;
  padding: 0.5rem 0.7rem;
  min-height: 44px;
  border-radius: 10px;
  cursor: pointer;
}

.signin-alt__button:hover {
  background: var(--teal-050);
}

/* --- signed-in shell ------------------------------------------------------ */

.shell {
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}

.shell__header {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  padding: calc(0.55rem + var(--safe-top)) 1rem 0.55rem;
  /* iOS bar: translucent + blurred, content slides beneath it. */
  background: color-mix(in srgb, var(--card) 86%, transparent);
  -webkit-backdrop-filter: blur(14px) saturate(1.6);
  backdrop-filter: blur(14px) saturate(1.6);
  border-bottom: 1px solid var(--line-soft);
  position: -webkit-sticky;
  position: sticky;
  top: 0;
  z-index: 10;
}

.shell__brand {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-right: auto;
}

.shell__logo {
  width: 40px;
  height: auto;
}

.shell__name {
  font-weight: 700;
  letter-spacing: -0.01em;
}

.shell__main {
  flex: 1 1;
  padding-bottom: calc(var(--tabbar-h) + var(--safe-bottom) + 0.6rem);
}

/* --- bottom tab bar (iOS: icon over tiny label, tint = active) ------------ */

.tabbar {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  display: flex;
  height: calc(var(--tabbar-h) + var(--safe-bottom));
  background: color-mix(in srgb, var(--card) 88%, transparent);
  -webkit-backdrop-filter: blur(14px) saturate(1.6);
  backdrop-filter: blur(14px) saturate(1.6);
  border-top: 1px solid var(--line-soft);
  padding-bottom: var(--safe-bottom);
  z-index: 10;
}

.tabbar__tab {
  flex: 1 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-height: 44px;
  padding: 4px 0.4rem 2px;
  font-size: 0.66rem;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--ink-soft);
  text-decoration: none;
}

.tabbar__icon {
  display: block;
  width: 24px;
  height: 24px;
}

.tabbar__tab--active {
  color: var(--teal-ink);
}

/* --- toasts --------------------------------------------------------------- */

.toast-stack {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(var(--tabbar-h) + var(--safe-bottom) + 0.6rem);
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  z-index: 300;
  width: min(92vw, 24rem);
  pointer-events: none;
}

.toast {
  pointer-events: auto;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  background: var(--ink);
  color: var(--paper);
  border-radius: 10px;
  padding: 0.65rem 0.85rem;
  box-shadow: var(--shadow);
  animation: toast-in 180ms ease-out;
}

.toast--error {
  background: var(--err);
}

.toast__message {
  flex: 1 1;
  font-size: 0.9rem;
}

.toast__dismiss {
  font: inherit;
  background: transparent;
  border: none;
  color: inherit;
  opacity: 0.8;
  cursor: pointer;
  padding: 0.1rem 0.3rem;
}

@keyframes toast-in {
  from {
    opacity: 0;
    transform: translateY(6px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* --- team chat -------------------------------------------------------------
 *
 * Phone-first, no new colours: unread uses the brand gold (the palette's own
 * "used sparingly" accent — a small notification pill is exactly that use),
 * my-message bubbles use the teal fill already used for primary buttons so
 * a sent message reads as "mine" the same way a submit button reads as the
 * primary action anywhere else in the app.
 */

/* One inset-grouped card, hairline separators between rows — not a stack of
   bordered boxes. */
.channel-list {
  list-style: none;
  margin: 0;
  padding: 0;
  background: var(--card);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  overflow: hidden;
}

.channel-list li + li {
  border-top: 1px solid var(--line-soft);
}

.channel-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  min-height: 60px;
  padding: 0.7rem 1rem;
  text-decoration: none;
  color: var(--ink);
}

.channel-row:active {
  background: var(--teal-050);
}

.channel-row__main {
  flex: 1 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}

.channel-row__name {
  font-weight: 700;
  color: var(--ink);
}

.channel-row__preview {
  font-size: 0.85rem;
  color: var(--ink-soft);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.channel-row__meta {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.35rem;
  flex-shrink: 0;
}

.channel-row__time {
  font-size: 0.75rem;
  color: var(--ink-soft);
}

/* Unread: a quiet dot, iOS Messages-style — the count lives in aria. */
.channel-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--gold);
}

.chat-thread-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

/* Inner screen: iOS drops to a compact bar title, one line, never wrapping. */
.chat-thread-head .page__title {
  font-size: 1.25rem;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.chat-thread-head__back {
  order: -1;
  margin-right: 0.25rem;
  flex-shrink: 0;
}

.page--chat {
  display: flex;
  flex-direction: column;
  min-height: calc(100dvh - 3.6rem);
}

.chat-thread {
  display: flex;
  flex-direction: column;
  flex: 1 1;
  gap: 0.75rem;
}

.chat-messages {
  flex: 1 1;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

.chat-message {
  display: flex;
  align-items: flex-end;
  gap: 0.5rem;
  max-width: 85%;
}

.chat-message--mine {
  align-self: flex-end;
  flex-direction: row-reverse;
}

.chat-message__avatar {
  flex-shrink: 0;
  width: 1.9rem;
  height: 1.9rem;
  border-radius: 50%;
  background: var(--teal-100);
  color: var(--teal-ink);
  font-size: 0.7rem;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
}

.chat-bubble {
  background: var(--card);
  border-radius: 18px;
  padding: 0.55rem 0.85rem;
  min-width: 0;
  box-shadow: 0 1px 2px rgba(6, 50, 44, 0.07);
}

.chat-message--mine .chat-bubble {
  background: var(--teal-700);
  border-color: var(--teal-700);
  color: #fff;
}

.chat-bubble__author {
  display: block;
  font-size: 0.75rem;
  font-weight: 700;
  color: var(--teal-ink);
  margin-bottom: 0.1rem;
}

.chat-bubble__body {
  margin: 0;
  font-size: 0.94rem;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.chat-bubble__time {
  display: block;
  margin-top: 0.2rem;
  font-size: 0.68rem;
  opacity: 0.7;
}

.chat-composer {
  /* PINNED, iOS-style: sticky against the document scroll (the page is the
     scroll container), offset by the fixed tab bar's height, so the thread
     scrolls beneath it. It keeps its flow position at scroll-bottom — the
     last message is never covered — and carries an opaque-ish blurred
     background for when older messages slide under. */
  position: -webkit-sticky;
  position: sticky;
  bottom: calc(var(--tabbar-h) + var(--safe-bottom));
  display: flex;
  align-items: flex-end;
  gap: 0.5rem;
  background: color-mix(in srgb, var(--paper) 90%, transparent);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  padding: 0.55rem 0;
  margin-top: 0.4rem;
  border-top: 1px solid var(--line-soft);
}

.chat-composer__input {
  flex: 1 1;
  font: inherit;
  resize: none;
  max-height: 6rem;
  min-height: 44px;
  padding: 0.62rem 1rem;
  border: 1px solid var(--line);
  border-radius: 22px;
  background: var(--card);
  color: var(--ink);
}

.chat-composer__input:focus-visible {
  outline: 2px solid var(--teal-600);
  outline-offset: 1px;
}

/* The round arrow-up send, exactly the platform's own shape. */
.chat-composer__send {
  flex-shrink: 0;
  width: 44px;
  height: 44px;
  padding: 0;
  border-radius: 50%;
  font-size: 1.25rem;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/* --- THE ENTRANCE --------------------------------------------------------
 *
 * A sign-in should feel like arriving somewhere, so the app does not simply
 * appear: two deep-water panels stand shut, Cuboid's own logo BUILDS ITSELF
 * out of its own cubes on the seam, the eye lands and blinks, the fish bobs
 * while bubbles rise past it — and only then does the water part.
 *
 * WHERE THE NUMBERS LIVE: src/lib/logo.ts (the beats) and
 * src/lib/gate-transition.ts (the total). Anything timed per-element arrives
 * as an inline custom property from LogoAssembly.tsx rather than being typed
 * again here, because a duration that exists twice eventually disagrees with
 * itself. The two totals that DO appear below (2200ms open / 800ms close) are
 * covered by tests/logo.test.mjs, which fails if the beats outgrow them.
 *
 * TRANSFORM AND OPACITY ONLY. Every animated property here is composited —
 * no width/top/left/filter animation anywhere, so 60 flying tiles cost the
 * main thread nothing. The tiles are torn out of the DOM the moment they
 * land (LogoAssembly.tsx), which is why `will-change` is safe to set on them.
 */

.app-gate {
  position: fixed;
  inset: 0;
  z-index: 800;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}

/* ONE FIELD OF WATER, NOT TWO SLIDING PANELS. The panels were the old
   interstitial's idea: something to hold shut while you waited. Nothing waits
   now — the fish carries you — so the overlay is simply the tank, fading in
   under the transition and out from under it. */
.app-gate__water {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(120% 70% at 50% -10%, rgba(199, 236, 229, 0.16), transparent 62%),
    linear-gradient(180deg, var(--teal-900) 0%, var(--teal-800) 62%, #0d6f61 100%);
}

/* THERE IS NO `--open` RULE, AND ITS ABSENCE IS THE FIX.
 *
 * `.app-gate--open .app-gate__water` ran `gate-water-out` on arrival: opacity
 * 1 held through the first 55% of 420ms, then faded. Measured on the running
 * page it was covering the app at opacity 1.00 with NOTHING inside it — the
 * fish that used to break up in there moved to the landing. An opaque teal
 * rectangle on a timer is exactly what the operator saw ("keeps it blue
 * background then flashes", 2026-08-10), and the flash was `stage-enter`
 * being cut off mid-fade when the phase went idle.
 *
 * Both are gone. Water now only ever fades IN and HOLDS — over the form you
 * are leaving (`--signin`) or over the app you are leaving (`--close`).
 * Nothing fades water away over an app you have just arrived in, because
 * nothing covers it in the first place. */

/* THE SIGN-IN COVER RISES AND HOLDS — IT NEVER OPENS.
   This runs on the /signin fallback route while auth is in flight, so what is
   behind it is the form you just left. It fades IN and then STAYS opaque for
   as long as the request takes; holding is what keeps it seamless at any
   latency. */
.app-gate--signin .app-gate__water {
  animation: gate-water-in 220ms cubic-bezier(0.4, 0, 0.2, 1) both;
}

.app-gate--close .app-gate__water {
  animation: gate-water-in var(--gate-close-ms, 770ms) cubic-bezier(0.4, 0, 0.2, 1) both;
}

@keyframes gate-water-in {
  from { opacity: 0; }
  40% { opacity: 1; }
  to { opacity: 1; }
}

.app-gate__content {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.1rem;
}

@keyframes gate-content-appear {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* --- the fish, and its cubes ---------------------------------------------
 * ONE TILE SYSTEM, FOUR STATES (operator, 2026-08-09). The landing's tap
 * shatter, the sign-in reassembly and the sign-out return all move the SAME
 * hexagonal cubes between the SAME two poses: in formation, and scattered.
 * Only the direction and the stagger change. The lattice is measured off the
 * real PNG (src/lib/logo.ts) and each piece is `clip-path`ed to a pointy-top
 * hexagon, so a piece is always a whole cube — never a rectangular sliver,
 * which is the defect this replaced.
 */

.fish {
  position: relative;
  /* Sized off the SMALLER viewport axis so a 375px phone and a desktop both
     get a mark that fills the same share of the screen. */
  width: min(58vmin, 340px);
  aspect-ratio: 862 / 544;
}

.fish__frame {
  position: absolute;
  inset: 0;
}

/* --- THE HANDOFF: the page's own mark becomes the transition's -------------
 * Operator, 2026-08-10: "the fish logo from that page should be the one that
 * animates and breaks apart like the original landing page so we don't flash
 * another fish thing in front — make it seamless login".
 *
 * `--mark-dx/--mark-dy/--mark-scale` are written by React from `markHandoff`
 * (src/lib/logo.ts) in a LAYOUT effect, so they are on the element before the
 * first paint. They map this box — which is already sitting at its
 * destination — exactly onto the sign-in sheet's own logo; animating the
 * transform to identity walks it back to where it was always going.
 *
 * THE SCALE CANNOT BE NEGATIVE. It is clamped positive at its source, in
 * `markHandoff`, not here — a mirrored client logo is the defect this
 * codebase has now shipped twice, and `tests/landing-motion.test.mjs`
 * resolves `--mark-scale` to that clamp's floor when it proves no transform
 * in this stylesheet can reflect anything. The fallback `1` is a second
 * floor for a browser that somehow paints before React writes the variable. */
.fish--handoff {
  transform-origin: 50% 50%;
  will-change: transform;
  animation: mark-handoff var(--mark-handoff-ms, 420ms)
    cubic-bezier(0.34, 0.02, 0.2, 1) both;
}

@keyframes mark-handoff {
  from {
    transform: translate3d(var(--mark-dx, 0px), var(--mark-dy, 0px), 0)
      scale(var(--mark-scale, 1));
  }
  to {
    transform: translate3d(0, 0, 0) scale(1);
  }
}

/* THE HOLD — what a slow sign-in looks like, and the pose the next document
 * picks up from.
 *
 * The arrival used to end on `fish-swim-across`, a looping +/-14vw traverse.
 * That looked fine and hid a seam: auth landed at an arbitrary point in the
 * loop, so the fish could be anywhere across the screen when the redirect
 * fired — while the receiving document always began its break-up dead centre.
 * The jump was real and invisible only because it happened across a document
 * swap.
 *
 * This breathes IN PLACE instead: under 2% of drift and about a degree of
 * tilt, so wherever a truncation falls, the pose is within a pixel or two of
 * the pose beat 4 starts from. And because both documents run this same loop
 * from mount, the phase itself can travel — GateProvider resumes it with a
 * negative delay computed from the entry mark's own timestamp
 * (`holdPhaseMs`), so the breath continues rather than restarting. */
.fish__hold {
  position: absolute;
  inset: 0;
  will-change: transform;
  animation: fish-hold 4200ms ease-in-out infinite;
}

@keyframes fish-hold {
  0% { transform: translate3d(-1.4%, 0, 0) rotate(0deg); }
  30% { transform: translate3d(0.4%, -1.5%, 0) rotate(-0.9deg); }
  62% { transform: translate3d(1.4%, 0.6%, 0) rotate(0.5deg); }
  100% { transform: translate3d(-1.4%, 0, 0) rotate(0deg); }
}

/* --- THE MARK IS NEVER MIRRORED, AND NEVER CARRIED AWAY -------------------
 * Three operator corrections to ONE gesture, and this is where they landed.
 *
 * 1. "the fish turns the other way" — the landing's idle held `scaleX(-1)`
 *    for ~15 seconds of every 29. Removed.
 * 2. "STILL on sign out the fish turns the other way" — the departure kept
 *    the same flip for its whole run, so leaving the app drew the client's
 *    logo backwards: eye left, tail right, the reverse of their storefront.
 *    Removed. A brand mark is not a sprite; it has a canonical facing and
 *    there is no state, transition or single frame in which reversing it is
 *    acceptable.
 * 3. "now at signout the fish still travles backwards just wanted it to fade
 *    into the background and bring the fish on then break out" — with the
 *    flip gone, what was left was the TRAVEL: `fish-swim-back` slid the mark
 *    11vw → -16vw. The comment that used to sit here argued that the opposite
 *    PATH was what made a departure legible. It was wrong, and the operator
 *    is the authority on what "backwards" means to the person watching: the
 *    fish being carried off screen away from them.
 *
 * SO THE DEPARTURE DOES NOT MOVE. `fish-swim-back` and `.fish__swim--back`
 * are deleted, not emptied — a keyframe that still exists gets reused. The
 * fish assembles in place, holds a beat on the same in-place breath the
 * arrival uses, and breaks out exactly where it stood (FishArc / DEPARTURE).
 * Arriving and leaving are now the same gesture in mirror-image TIME, which
 * is the only kind of mirroring this mark permits.
 *
 * MECHANICALLY ENFORCED, NOT REMEMBERED. tests/landing-motion.test.mjs walks
 * every transform this stylesheet applies to the mark or its cubes, builds
 * the 2x2 matrix, and fails on a reflection OR on any horizontal travel along
 * the departure path. Three times was two too many.
 */
.fish__swim {
  position: absolute;
  inset: 0;
  will-change: transform;
}

.fish__static,
.fish__grid {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* The assembled mark — the client's file, whole and untouched. */
.fish__static {
  display: block;
  object-fit: contain;
}

/* ONE CUBE. Same image, shifted background-position, clipped to the artwork's
   own hexagon — so what flies is a cube of the client's real pixels. */
.fish__tile {
  position: absolute;
  background-repeat: no-repeat;
  transform-origin: 50% 50%;
  will-change: transform, opacity;
  animation-fill-mode: both;
}

.fish__tile--in {
  animation-name: fish-tile-in;
  /* Overshoot on the way in: the cubes SETTLE into place instead of stopping
     dead, which is what makes an assembly read as physical. */
  animation-timing-function: cubic-bezier(0.18, 0.86, 0.28, 1.06);
}

.fish__tile--out {
  animation-name: fish-tile-out;
  /* Accelerating away — pieces of something breaking do not ease out. */
  animation-timing-function: cubic-bezier(0.45, 0, 0.85, 0.4);
}

@keyframes fish-tile-in {
  from {
    opacity: 0;
    transform: translate3d(var(--tile-dx), var(--tile-dy), 0)
      rotate(var(--tile-rot)) scale(var(--tile-scale));
  }
  55% { opacity: 1; }
  to { opacity: 1; transform: translate3d(0, 0, 0) rotate(0deg) scale(1); }
}

@keyframes fish-tile-out {
  from { opacity: 1; transform: translate3d(0, 0, 0) rotate(0deg) scale(1); }
  55% { opacity: 0.85; }
  to {
    opacity: 0;
    transform: translate3d(var(--tile-dx), var(--tile-dy), 0)
      rotate(var(--tile-rot)) scale(var(--tile-scale));
  }
}

/* --- reduced motion ------------------------------------------------------- */
/* The whole sequence collapses to a 200ms crossfade over the STILL mark: no
   panels moving, no tiles, no bubbles, no blink. LogoAssembly stops rendering
   the tiles and bubbles at all in this mode — this block is the belt-and-
   braces for a preference that flips mid-animation. */

.app-gate--reduced .app-gate__water {
  animation: none;
}

/* Only `--close` remains: there is no arrival overlay to fade away, under
   this preference or any other. */
.app-gate--reduced.app-gate--close {
  animation: gate-reduced-in 200ms ease forwards;
}

.app-gate--reduced .app-gate__content {
  animation: none;
}

@keyframes gate-reduced-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .app-gate__water,
  .app-gate__content,
  .fish__tile,
  .fish__swim,
  /* The shared-element handoff and its hold are motion too. With the
     animation off, `.fish--handoff` never applies a transform at all, so the
     mark simply IS at its destination — an instant cut, which is what this
     preference asks for. */
  .fish--handoff,
  .fish__hold {
    animation: none !important;
    animation-delay: 0ms !important;
  }
  .fish__grid {
    display: none;
  }
  /* THE TANK GOES STILL: no idle drift, no light drift, no plant sway, no
     bubbles — the mark, held, on still water. Delays are zeroed as well as
     durations: a "no motion" path that still waits out a stagger is a stall,
     not an accommodation. (The wander's four loops are gone entirely now —
     `.landing__idle` is the only motion the mark has left, and it is stopped
     here along with everything else.) */
  .landing__anchor,
  .landing__idle,
  .landing__fish,
  .landing__water,
  .landing__caustics span,
  .landing__weed {
    animation: none !important;
    animation-delay: 0ms !important;
  }
  .landing__bubbles {
    display: none;
  }
  /* THE SHATTER IS AN INSTANT CUT (operator ask + S22). LandingShatter never
     mounts the tiles under this preference — it lets the link navigate — but
     a preference that flips mid-flight must not leave 60 tiles animating, so
     every part of the break-up is stopped here too. Stagger delays are
     zeroed rather than merely paused: a "no motion" path that still waits
     out a stagger is a stall, not an accessibility accommodation. */
  /* THE WHOLE RECOMPOSITION IS AN INSTANT CUT TOO. No lift, no rise, no drop
     — the panel is simply already open at its centred position and the fish
     is simply already in its berth. Same accommodation the shatter gets, for
     the same reason: a "no motion" path that still plays a 420ms gesture is
     not one. With the lift's animation off, `.landing__anchor` carries no
     transform at all, so the mark stays exactly where it always was. */
  .landing__stage--panel .landing__anchor,
  .landing__stage--closing .landing__anchor,
  .landing__stage--panel .landing-panel__sheet,
  .landing__stage--panel .landing-panel__scrim,
  .landing__stage--closing .landing-panel__sheet,
  .landing__stage--closing .landing-panel__scrim,
  .landing__stage--panel .landing__content,
  .landing__stage--dismissing .landing__content,
  .landing__stage--depart .landing-panel__sheet,
  .landing__stage--depart .landing-panel__scrim,
  .landing__stage--closing .landing__content,
  .landing__stage--depart .landing__content,
  .landing:has(.landing__stage--depart) .landing__bubbles,
  .landing:has(.landing__stage--depart) .landing__footer,
  .landing:has(.landing__stage--depart) .landing__caustics,
  .landing:has(.landing__stage--depart) .landing__plants,
  .landing:has(.landing__stage--depart) .landing__water {
    animation: none !important;
    animation-delay: 0ms !important;
  }
}

/* The stage. THE APP DOES NOT FADE IN — it is simply there.
 *
 * `.app-stage--entering` used to run `stage-enter`: opacity 0.2 → 1 over
 * 900ms after a 130ms delay. Because the gate returned to idle at 420ms, the
 * class was removed mid-animation and the app SNAPPED from about 46% to 100%
 * in one frame. That snap, under an opaque teal cover, was the "flash". Both
 * the class and the keyframes are deleted rather than retimed: the app has
 * nothing to fade in from now that nothing covers it, and a 900ms fade over
 * a screen the user is already looking at is latency with a curve on it.
 *
 * Leaving still dims, because leaving still has a gesture over it. */
.app-stage {
  min-height: 100dvh;
}

.app-stage--leaving {
  animation: stage-leave 800ms cubic-bezier(0.65, 0, 0.35, 1) forwards;
}

@keyframes stage-leave {
  from {
    opacity: 1;
    transform: scale(1);
  }
  to {
    opacity: 0.35;
    transform: scale(0.99);
  }
}

/* --- schedule / coverage grid (cuboid/coverage-grid) ----------------------
 * The headline feature (spec §2): a phone works ONE DAY AT A TIME (day
 * tabs) rather than trying to cram 7 days onto a 375px screen the way the
 * source spreadsheet does — that density is exactly what this app beats the
 * sheet on. The hour table itself still scrolls horizontally inside its own
 * container on the narrowest phones (17 hour columns), never the page.
 */

/* iOS segmented control: a soft track, the active segment is a floating
   white pill — no borders anywhere. */
.schedule-toggle {
  display: flex;
  gap: 2px;
  padding: 2px;
  border-radius: 12px;
  background: color-mix(in srgb, var(--line) 42%, var(--paper));
  margin-bottom: 0.9rem;
}

.schedule-toggle__tab {
  flex: 1 1;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 40px;
  text-align: center;
  padding: 0.45rem 0.6rem;
  border-radius: 10px;
  color: var(--ink-soft);
  font-weight: 600;
  font-size: 0.9rem;
  text-decoration: none;
}

.schedule-toggle__tab--active {
  background: var(--card);
  color: var(--ink);
  box-shadow: 0 1px 4px rgba(6, 50, 44, 0.16);
}

.weeknav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  margin: 0 0 0.9rem;
}

.weeknav__step {
  flex: none;
  padding: 0.45rem 0.7rem;
  border-radius: 8px;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--teal-ink);
  text-decoration: none;
  white-space: nowrap;
}

.weeknav__step:hover {
  background: var(--teal-050);
}

.weeknav__label {
  flex: 1 1;
  text-align: center;
  font-weight: 700;
  font-size: 0.95rem;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  flex-wrap: wrap;
}

.weeknav__current {
  font-size: 0.62rem;
}

.daytabs {
  display: flex;
  gap: 0.3rem;
  overflow-x: auto;
  padding-bottom: 0.2rem;
  margin-bottom: 0.6rem;
  /* iOS shows no persistent scrollbar under a pill strip. */
  scrollbar-width: none;
}

.daytabs::-webkit-scrollbar {
  display: none;
}

.daytab {
  font: inherit;
  flex: none;
  min-height: 40px;
  padding: 0.45rem 0.85rem;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--card);
  color: var(--ink-soft);
  font-weight: 600;
  font-size: 0.85rem;
  cursor: pointer;
}

.daytab--active {
  background: var(--teal-700);
  border-color: var(--teal-700);
  color: #fff;
}

.grid-scroll {
  overflow-x: auto;
  border-radius: var(--radius);
  background: var(--card);
  box-shadow: var(--shadow);
  -webkit-overflow-scrolling: touch;
  /* Keep the affordance, lose the gutter: a slim rounded thumb. */
  scrollbar-width: thin;
  scrollbar-color: var(--line) transparent;
}

.grid-scroll::-webkit-scrollbar {
  height: 4px;
}

.grid-scroll::-webkit-scrollbar-thumb {
  background: var(--line);
  border-radius: 999px;
}

.grid-scroll::-webkit-scrollbar-track {
  background: transparent;
}

.coverage-table {
  border-collapse: collapse;
  width: 100%;
  font-size: 0.78rem;
}

.coverage-table__who {
  position: -webkit-sticky;
  position: sticky;
  left: 0;
  z-index: 1;
  background: var(--card);
  text-align: left;
  padding: 0.4rem 0.5rem;
  min-width: 6.4rem;
  max-width: 6.4rem;
  border-bottom: 1px solid var(--line);
  border-right: 1px solid var(--line);
  vertical-align: middle;
}

thead .coverage-table__who {
  z-index: 2;
}

.coverage-table__name {
  display: block;
  font-weight: 700;
  font-size: 0.82rem;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.coverage-table__markers {
  display: flex;
  gap: 0.25rem;
  margin-top: 0.2rem;
}

.coverage-table__hour {
  padding: 0.35rem 0.2rem;
  text-align: center;
  font-weight: 600;
  color: var(--ink-soft);
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
  min-width: 2.1rem;
}

.coverage-table__cell {
  text-align: center;
  padding: 0.25rem;
  border-bottom: 1px solid var(--line);
}

.coverage-table__coverage-row .coverage-table__who {
  font-weight: 700;
  font-size: 0.78rem;
  border-bottom: none;
}

.coverage-table__coverage-row td {
  border-bottom: none;
  border-top: 2px solid var(--line);
}

.hourcell {
  display: inline-block;
  width: 1.6rem;
  height: 1.6rem;
  border-radius: 6px;
  border: 1px solid var(--line);
  background: var(--paper);
  padding: 0;
  cursor: default;
}

button.hourcell {
  cursor: pointer;
}

button.hourcell:not(:disabled):hover {
  border-color: var(--teal-600);
}

.hourcell--on {
  background: var(--teal-600);
  border-color: var(--teal-700);
}

.hourcell--pending {
  opacity: 0.55;
}

.coverage-count {
  display: inline-block;
  min-width: 1.4rem;
  font-weight: 700;
  color: var(--teal-ink);
}

.coverage-count--thin {
  color: var(--err);
}

.chip {
  font: inherit;
  display: inline-block;
  padding: 0.1rem 0.4rem;
  border-radius: 999px;
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.03em;
  border: 1px solid var(--line);
  background: var(--card);
  color: var(--ink-soft);
  cursor: default;
}

button.chip {
  cursor: pointer;
}

.chip--active.chip--off {
  background: var(--err-bg);
  color: var(--err);
  border-color: var(--err);
}

.chip--active.chip--close {
  background: var(--gold-soft);
  color: #7a5c0e;
  border-color: var(--gold);
}

.hours-rail {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
}

.hours-rail__row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  min-height: 2.1rem;
  font-size: 0.9rem;
}

.hours-rail__row + .hours-rail__row {
  border-top: 1px solid var(--line-soft);
}

.hours-rail__value {
  font-weight: 700;
  color: var(--teal-ink);
}

.schedule-footer__row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.2rem 0;
  font-size: 0.95rem;
}

.schedule-footer__row + .schedule-footer__row {
  border-top: 1px solid var(--line-soft);
  margin-top: 0.3rem;
  padding-top: 0.5rem;
}

.myweek-total {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  margin: 0.2rem 0 1rem;
}

.myweek-total__value {
  font-size: 2.4rem;
  font-weight: 800;
  color: var(--teal-ink);
  letter-spacing: -0.02em;
}

.myweek-total__label {
  color: var(--ink-soft);
  font-size: 0.95rem;
}

.myday__head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 0.5rem;
}

.myday__name {
  font-weight: 700;
  font-size: 1.02rem;
}

.myday__body {
  margin: 0.35rem 0 0;
  font-size: 1rem;
  color: var(--ink);
}

.myday__body--empty {
  color: var(--ink-soft);
}

/* --- tasks (cuboid/task-rotation, spec §3) --------------------------------
 * No new colours — every fill/border below reuses the existing brand
 * tokens (--teal-*, --gold, --ink*, --line, --paper) from :root above.
 */

.task-cadence {
  font-style: italic;
}

.task-owners {
  font-size: 0.85rem;
}

.task-dashboard-role {
  text-transform: capitalize;
  font-size: 0.85rem;
  margin: 0 0 0.5rem;
}

.task-steps {
  list-style: none;
  margin: 0.6rem 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.task-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  min-height: 44px;
  padding: 0.5rem 0.75rem;
  border-radius: 10px;
  background: var(--paper);
}

.task-row__text {
  font-size: 0.95rem;
}

.task-row__owner {
  color: var(--ink-soft);
  font-size: 0.85rem;
}

.task-row__pending {
  font-size: 0.75rem;
  color: var(--ink-soft);
}

.task-steps--readonly .task-row {
  background: transparent;
  border: 1px solid var(--line-soft);
  min-height: 0;
  padding: 0.4rem 0.75rem;
}

/* The clean week header above a rotation screen: small-caps label, no box. */
.week-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  flex-wrap: wrap;
}

.week-head__label {
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.week-head .progress {
  flex: 1 1 100%;
  margin: 0.2rem 0 0;
}

.task-check {
  white-space: nowrap;
}

.task-check--done {
  color: var(--teal-ink);
}

/* --- progress bar ----------------------------------------------------- */

.progress {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0.6rem 0;
  flex-wrap: wrap;
  width: 100%;
}

.progress__label {
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--ink-soft);
  min-width: 6.5rem;
}

.progress__track {
  flex: 1 1;
  min-width: 5rem;
  height: 8px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.08);
  overflow: hidden;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.04);
}

.progress__fill {
  height: 100%;
  background: var(--grad-cube);
  border-radius: 999px;
  transition: width 420ms cubic-bezier(0.22, 1, 0.36, 1);
}

.progress__value {
  font-family: var(--font-display);
  font-size: 0.85rem;
  font-weight: 700;
  font-feature-settings: "tnum";
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  white-space: nowrap;
}

/* --- task admin: the editable list + the editor ---------------------------
 * cuboid/task-admin. The controls David actually touches on a phone, in the
 * app's existing iOS language: inset grouped rows, 44px targets, no
 * horizontal scroll at 375px (every multi-column control below is a grid
 * that divides the available width rather than a row that overflows it).
 *
 * Replaces the old .task-pin select — one week-pin dropdown per task card,
 * superseded by the editor's week toggles, which can say "weeks 1 and 3".
 */

/* Visible to a screen reader, not to the eye — for a control whose visual
   label is its placeholder (the task filter). */
.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;
}

.stack--tight {
  gap: 0.5rem;
}

/* The quiet header above a grouped list ("Assigned to", "Retired"). */
.section-title {
  margin: 0.4rem 0 0;
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

/* A labelled input living inside a .group__row — label left, value right,
   the way an iOS settings form reads. */
.formrow {
  gap: 0.6rem;
}

.formrow__label {
  flex-shrink: 0;
  font-weight: 600;
  font-size: 0.95rem;
}

.formrow__input {
  font: inherit;
  flex: 1 1;
  min-width: 0;
  border: none;
  background: transparent;
  color: var(--ink);
  text-align: right;
  padding: 0.2rem 0;
  min-height: 32px;
}

.formrow__input::placeholder {
  color: color-mix(in srgb, var(--ink-soft) 60%, var(--paper));
}

.formrow__input:focus-visible {
  outline: 2px solid var(--teal-600);
  outline-offset: 2px;
  border-radius: 6px;
}

.formrow__input--select {
  /* Native select keeps its own chevron; nudge it off the right edge. */
  text-align: right;
  text-align-last: right;
}

/* − 3× a month + */
.stepper {
  margin-top: 0.6rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}

.stepper__btn {
  font: inherit;
  font-size: 1.35rem;
  line-height: 1;
  font-weight: 600;
  min-width: 56px;
  min-height: 44px;
  border-radius: 12px;
  border: 1px solid var(--line);
  background: var(--card);
  color: var(--teal-ink);
  cursor: pointer;
}

.stepper__btn:disabled {
  opacity: 0.4;
  cursor: default;
}

.stepper__btn:not(:disabled):active {
  background: var(--teal-050);
}

.stepper__value {
  flex: 1 1;
  text-align: center;
  font-size: 1.15rem;
  font-weight: 700;
  letter-spacing: -0.01em;
}

.stepper__unit {
  font-size: 0.85rem;
  font-weight: 500;
  color: var(--ink-soft);
}

/* Week 1 | Week 2 | Week 3 | Week 4 — toggles in the editor, links in the
   report. Four equal columns so it never overflows a 375px phone. */
.weekpick {
  margin-top: 0.6rem;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-gap: 0.4rem;
  gap: 0.4rem;
}

.weekpick--nav {
  margin: 0 0 0.9rem;
}

.weekpick__btn {
  font: inherit;
  font-size: 0.82rem;
  font-weight: 600;
  min-height: 44px;
  padding: 0.35rem 0.2rem;
  border-radius: 12px;
  border: 1px solid var(--line);
  background: var(--card);
  color: var(--ink-soft);
  cursor: pointer;
  text-decoration: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.1rem;
}

.weekpick__btn--on {
  background: var(--teal-700);
  border-color: var(--teal-700);
  color: #fff;
}

.weekpick__btn:disabled {
  opacity: 0.5;
  cursor: default;
}

/* The week's own Monday under its number. Four of these sit across a phone,
   so it is the SHORT form (store-time.ts#formatWeekStartLabel) and is pinned
   to one line — a wrapped date would push the 44px tap target out of shape. */
.weekpick__dates {
  font-size: 0.66rem;
  font-weight: 600;
  opacity: 0.85;
  white-space: nowrap;
}

.weekpick__now {
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  opacity: 0.75;
}

/* A tap-to-tick roster row inside a .group. */
.pickrow {
  font: inherit;
  width: 100%;
  text-align: left;
  border: none;
  background: transparent;
  cursor: pointer;
}

.pickrow:not(:disabled):active {
  background: var(--teal-050);
}

.pickrow__tick {
  flex-shrink: 0;
  width: 1.4rem;
  text-align: center;
  font-size: 1.1rem;
  font-weight: 700;
  color: var(--teal-700);
}

/* The filter above the 160-row task list. */
.tasksearch {
  display: block;
}

.tasksearch__input {
  font: inherit;
  width: 100%;
  padding: 0.7rem 0.85rem;
  min-height: 44px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--card);
  color: var(--ink);
}

.tasksearch__input:focus-visible {
  outline: 2px solid var(--teal-600);
  outline-offset: 1px;
}

.tasksearch__count {
  margin: 0;
}

/* "2/3" done-this-week, trailing a task row. */
.taskcount {
  flex-shrink: 0;
  font-size: 0.78rem;
  font-weight: 700;
  font-feature-settings: "tnum";
  font-variant-numeric: tabular-nums;
  color: var(--ink-soft);
}

.taskcount--done {
  color: var(--teal-ink);
}

/* The employee's own week-by-week record card. */
.recordrows {
  margin-top: 0.6rem;
}

.task-cycle-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  flex-wrap: wrap;
}

.task-dashboard-back {
  padding-left: 0;
  margin-bottom: 0.4rem;
}

/* ====================================================================
 * SOL ULTRA REVIEW PASS — 2026-08-09
 * The phone day roster (S1), the task-list IA (S2), the Home disclosure (S9),
 * the chat list + day marks (S4/S19) and the wide-screen layout (S12).
 * ======================================================================== */

/* --- S1: the day roster (phones) vs the hourly matrix (tablet and up) -----
 * ONE BREAKPOINT DECIDES WHICH VIEW EXISTS. Below 700px the hourly table is
 * `display: none` — not merely scrolled — so a 375px document contains no
 * horizontally-overflowing element at all and `scrollWidth` equals the
 * viewport. Above it the roster steps aside and the matrix takes over,
 * exactly as it was.
 */

.schedule-phone {
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
}

@media (min-width: 700px) {
  .schedule-phone {
    display: none;
  }
}

@media (max-width: 699px) {
  .grid-scroll {
    display: none;
  }
}

/* The answer to David's actual question, above the list that supports it. */
.coverage-summary {
  padding: 0.85rem 1rem;
}

.coverage-summary__headline {
  margin: 0;
  font-size: 1.05rem;
  letter-spacing: -0.01em;
}

.coverage-summary__headline strong {
  font-size: 1.5rem;
  font-weight: 800;
  margin-right: 0.15rem;
}

.coverage-summary__span {
  font-weight: 600;
  color: var(--ink-soft);
  font-size: 0.9rem;
}

.coverage-summary__note {
  margin: 0.25rem 0 0;
  font-size: 0.84rem;
  color: var(--ink-soft);
}

/* The thin-coverage line is the one a manager needs to SEE, not scan past. */
.coverage-summary__note--thin {
  color: #92400e;
  font-weight: 600;
}

.coverage-summary__note--ok {
  color: var(--teal-ink);
  font-weight: 600;
}

.roster {
  overflow: visible;
}

.roster-row + .roster-row {
  border-top: 1px solid var(--line-soft);
}

.roster-row--open {
  background: color-mix(in srgb, var(--teal-050) 60%, var(--card));
}

.roster-row__head {
  font: inherit;
  text-align: left;
  width: 100%;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  min-height: 56px;
  padding: 0.65rem 1rem;
  background: none;
  border: none;
  color: var(--ink);
  cursor: pointer;
}

.roster-row__who {
  flex: 1 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.05rem;
}

.roster-row__name {
  font-weight: 700;
  font-size: 1rem;
}

.roster-row__sub {
  font-size: 0.76rem;
  color: var(--ink-soft);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.roster-row__when {
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.15rem;
}

.roster-row__shift {
  font-weight: 700;
  font-size: 0.92rem;
  color: var(--teal-ink);
  white-space: nowrap;
}

.roster-row__shift--out {
  color: var(--ink-soft);
  font-weight: 500;
}

.roster-row__hours {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  font-size: 0.75rem;
  color: var(--ink-soft);
  font-feature-settings: "tnum";
  font-variant-numeric: tabular-nums;
}

.roster-row__detail {
  padding: 0 1rem 0.9rem;
}

.roster-row__markers {
  display: flex;
  gap: 0.4rem;
  margin-bottom: 0.5rem;
}

.roster-row__hint {
  margin: 0 0 0.5rem;
  font-size: 0.78rem;
  color: var(--ink-soft);
}

/* Seventeen hours as a wrapping grid of 44px targets — the phone's answer to
   a row of seventeen table columns. */
.hourpick {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(3rem, 1fr));
  grid-gap: 0.35rem;
  gap: 0.35rem;
}

.hourpick__btn {
  font: inherit;
  font-size: 0.78rem;
  font-weight: 600;
  min-height: 44px;
  border-radius: 10px;
  border: 1px solid var(--line);
  background: var(--card);
  color: var(--ink-soft);
  cursor: pointer;
}

.hourpick__btn--on {
  background: var(--teal-700);
  border-color: var(--teal-700);
  color: #ffffff;
}

.hourpick__btn:disabled {
  opacity: 0.5;
}

.roster-rest__toggle {
  align-self: flex-start;
  padding-left: 0;
}

/* --- S9: Home's "everyone else today", folded ---------------------------- */

.dash-rest {
  margin: -0.35rem 0 0;
  padding: 0 1rem;
}

.dash-rest__summary {
  list-style: none;
  cursor: pointer;
  min-height: 40px;
  display: flex;
  align-items: center;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--ink-soft);
}

.dash-rest__summary::-webkit-details-marker {
  display: none;
}

.dash-rest__summary::after {
  content: "›";
  margin-left: 0.35rem;
  font-size: 1.1rem;
  line-height: 1;
  color: color-mix(in srgb, var(--ink-soft) 55%, var(--paper));
}

.dash-rest[open] .dash-rest__summary::after {
  content: "⌄";
}

.dash-rest__list {
  margin-bottom: 0.5rem;
}

/* --- S2: the task list's scope, filters and folds ------------------------ */

/* iOS segmented control, same language as .schedule-toggle. */
.scopetabs {
  display: flex;
  gap: 2px;
  padding: 2px;
  border-radius: 12px;
  background: color-mix(in srgb, var(--line) 42%, var(--paper));
}

.scopetab {
  font: inherit;
  flex: 1 1;
  min-height: 40px;
  border: none;
  border-radius: 10px;
  background: transparent;
  color: var(--ink-soft);
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
}

.scopetab--on {
  background: var(--card);
  color: var(--teal-ink);
  box-shadow: 0 1px 2px rgba(6, 50, 44, 0.12);
}

.taskfilters {
  display: flex;
  gap: 0.5rem;
}

.taskfilters .tasksearch {
  flex: 1 1;
  min-width: 0;
}

.taskfilters__who {
  flex-shrink: 0;
}

.taskfilters__select {
  font: inherit;
  min-height: 44px;
  max-width: 8.5rem;
  padding: 0.5rem 0.6rem;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--card);
  color: var(--ink);
}

.section-title {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.section-title__count {
  font-size: 0.72rem;
  font-weight: 700;
  color: var(--ink-soft);
  background: color-mix(in srgb, var(--line) 45%, var(--paper));
  border-radius: 999px;
  padding: 0.05rem 0.45rem;
  letter-spacing: 0;
}

/* A run of identical rows, collapsed to one line with its own count. */
.taskgroup__head {
  font: inherit;
  text-align: left;
  width: 100%;
  background: none;
  border: none;
  cursor: pointer;
}

.taskgroup__count {
  margin-left: 0.4rem;
  font-size: 0.75rem;
  font-weight: 700;
  color: var(--teal-ink);
  background: var(--teal-050);
  border-radius: 999px;
  padding: 0.05rem 0.4rem;
}

.taskgroup__child {
  padding-left: 2rem;
  background: color-mix(in srgb, var(--paper) 55%, var(--card));
}

.taskgroup__ordinal {
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--ink-soft);
}

/* THE FOLD IDIOM, SHARED. `.taskfold` was the task library's disclosure;
   the chat list's three sections use the same caret, the same type and the
   same rhythm, so the rules are shared by selector rather than copied into a
   second visual language. */
.fold,
.taskfold {
  border-radius: var(--radius);
}

.fold__summary,
.taskfold__summary {
  list-style: none;
  cursor: pointer;
  min-height: 44px;
  display: flex;
  align-items: center;
  font-size: 0.82rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.fold__summary::-webkit-details-marker,
.taskfold__summary::-webkit-details-marker {
  display: none;
}

.fold__summary::after,
.taskfold__summary::after {
  content: "›";
  margin-left: 0.4rem;
  font-size: 1.2rem;
  line-height: 1;
}

.fold[open] .fold__summary::after,
.taskfold[open] .taskfold__summary::after {
  content: "⌄";
}

.taskfold[open] .group {
  margin-bottom: 0.6rem;
}

/* --- S4 / S19: chat day marks and the channel row's two lines ------------ */

.chat-run {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

/* THE FIX FOR "6:09 PM above 5:49 PM": the day, stated between runs, so a
   thread spanning three days cannot read as one that is sorted backwards. */
.chat-daymark {
  margin: 0.35rem 0 0.1rem;
  text-align: center;
}

.chat-daymark span {
  display: inline-block;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-soft);
  background: color-mix(in srgb, var(--line) 40%, var(--paper));
  border-radius: 999px;
  padding: 0.15rem 0.65rem;
}

.channel-row__sender {
  font-weight: 700;
  color: var(--ink);
}

.channel-row__snippet {
  margin-left: 0.3rem;
}

/* --- S12: wide screens get a rail and a workspace, not a phone column ----
 * The app is phone-first and stays phone-first; but a 1440px browser showing
 * a 40rem column under a stretched bottom tab bar is a phone screenshot, not
 * a desktop app. At 900px and up the tab bar becomes a LEFT RAIL (the tabs
 * are already server-resolved per role — only their presentation changes)
 * and the content gets a real workspace to use.
 */

@media (min-width: 900px) {
  :root {
    /* The bar leaves the bottom of the screen, so every inset derived from
       its height (content padding, the sticky chat composer) collapses on
       its own — one variable, not four overrides that could drift. */
    --tabbar-h: 0px;
  }

  .shell {
    padding-left: 15rem;
  }

  .shell__header {
    padding-left: 1.6rem;
  }

  .shell__main {
    padding-bottom: 2rem;
  }

  .tabbar {
    top: 0;
    right: auto;
    bottom: 0;
    width: 15rem;
    height: 100dvh;
    flex-direction: column;
    justify-content: flex-start;
    gap: 0.25rem;
    padding: calc(4.2rem + var(--safe-top)) 0.75rem 1rem;
    border-top: none;
    border-right: 1px solid var(--line-soft);
    z-index: 9;
  }

  .tabbar__tab {
    flex: 0 0 auto;
    flex-direction: row;
    justify-content: flex-start;
    gap: 0.7rem;
    min-height: 46px;
    padding: 0 0.85rem;
    border-radius: 11px;
    font-size: 0.92rem;
  }

  .tabbar__tab--active {
    background: var(--teal-050);
  }

  /* Schedule and tasks are the two screens that were fighting the 40rem
     column — a coverage matrix and a 162-row list both read far better with
     room. Everything else keeps its comfortable measure. */
  .page {
    max-width: 46rem;
    padding: 1.2rem 1.6rem 2.5rem;
  }

  .page--wide {
    max-width: 75rem;
  }
}

@media (min-width: 900px) {
  /* Two columns of roster/summary would be a phone layout stretched; the
     wide screens get the matrix instead, so the phone block stays hidden. */
  .page--wide .stack {
    gap: 1.1rem;
  }
}

/* --- chat directory, DMs, groups, avatars (cuboid/chat-directory) --------
 *
 * Everything below extends the language that is already here — the inset
 * grouped list, the fold caret, the channel row — rather than introducing a
 * second one. The chat list's three sections ARE `.fold`s (shared selectors
 * above); a person row IS a `.group__row`; the member picker IS `.pickrow`.
 */

/* The chat list's section header carries a count and, crucially, the UNREAD
   ROLLUP — a collapsed section must never hide signal. */
.fold__summary {
  gap: 0.45rem;
}

.fold__title {
  flex-shrink: 0;
}

.fold__count {
  font-size: 0.72rem;
  font-weight: 700;
  color: var(--ink-soft);
  background: color-mix(in srgb, var(--line) 45%, var(--paper));
  border-radius: 999px;
  padding: 0.05rem 0.45rem;
  letter-spacing: 0;
}

/* The rollup. Gold, like the per-row unread dot, so the eye reads them as
   the same fact at two scales. Pushed to the right, ahead of the caret. */
.fold__unread {
  margin-left: auto;
  margin-right: 0.1rem;
  min-width: 1.25rem;
  text-align: center;
  font-size: 0.7rem;
  font-weight: 800;
  letter-spacing: 0;
  color: #3a2c00;
  background: var(--gold);
  border-radius: 999px;
  padding: 0.08rem 0.4rem;
}

.fold__empty {
  padding: 0.15rem 0.1rem 0.5rem;
}

.fold__emptytext {
  margin: 0;
  font-size: 0.85rem;
  color: var(--ink-soft);
}

.link {
  color: var(--teal-700);
  font-weight: 600;
  /* "start one" / "message someone" are invitations, not corrections — an
     underline made them read like errors in the empty state. */
  text-decoration: none;
}

.link:hover,
.link:focus-visible {
  text-decoration: underline;
}

/* One round action in a page header. 44px, so it is tappable with gloves. */
.iconbtn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: var(--card);
  color: var(--teal-ink);
  font-size: 1.5rem;
  line-height: 1;
  text-decoration: none;
}

.iconbtn:active {
  background: var(--teal-050);
}

/* A page title with one action beside it (Chat -> "New chat"). */
.page__head--action {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
}

/* --- avatars -------------------------------------------------------------
 * One chip, three sizes, two contents (a photo, or initials on the owner's
 * own deterministic colour). The colour comes from JS as an inline style —
 * it is data about a person, not a theme decision, so it cannot live here.
 */
.avatar {
  --avatar-size: 2.4rem;
  position: relative;
  flex-shrink: 0;
  display: inline-flex;
  width: var(--avatar-size);
  height: var(--avatar-size);
  border-radius: 50%;
  overflow: hidden;
  background: var(--card);
}

.avatar--sm {
  --avatar-size: 1.9rem;
}

.avatar--lg {
  --avatar-size: 3.6rem;
}

.avatar__img,
.avatar__initials {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  object-fit: cover;
  font-weight: 700;
  /* Scales with the chip so initials never overflow a small tile. */
  font-size: calc(var(--avatar-size) * 0.36);
  letter-spacing: 0.01em;
}

/* THE TEAMS COMPOSITE: up to four member tiles in a 2x2, so a group is
   recognisable by its people when it has no picture of its own. */
.avatar--composite {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-auto-rows: 1fr;
  grid-gap: 1px;
  gap: 1px;
  background: var(--line-soft);
}

.avatar--composite .avatar__tile {
  width: 100%;
  height: 100%;
  font-size: calc(var(--avatar-size) * 0.26);
  border-radius: 0;
}

/* Exactly two members: two half-moons rather than a 2x2 with holes. */
.avatar--composite .avatar__tile:first-child:nth-last-child(2),
.avatar--composite .avatar__tile:first-child:nth-last-child(2) ~ .avatar__tile {
  grid-row: span 2;
}

/* Three members: the first spans the left column, the other two stack. */
.avatar--composite .avatar__tile:first-child:nth-last-child(3) {
  grid-row: span 2;
}

.avatar--channel .avatar__initials {
  font-size: calc(var(--avatar-size) * 0.44);
}

/* --- presence ------------------------------------------------------------
 * The dot is drawn for exactly ONE state (src/lib/presence.ts: seen inside
 * the active window). There is deliberately no amber/grey variant: a
 * coloured dot for "away" invites the eye to read a status the timestamp
 * does not support, and the words beside it already say the truth.
 */
.presence-dot {
  position: absolute;
  right: -1px;
  bottom: -1px;
  width: 0.62rem;
  height: 0.62rem;
  border-radius: 50%;
  border: 2px solid var(--paper);
}

.presence-dot--on {
  background: #2e9e6b;
}

.channel-row__face,
.person-row__face {
  position: relative;
  flex-shrink: 0;
  display: inline-flex;
}

.person-row__presence {
  font-size: 0.75rem;
  color: var(--ink-soft);
}

.person-row__presence--on {
  color: #1f7350;
  font-weight: 600;
}

/* --- thread header: face, title, live status ---------------------------- */
.chat-thread-head__names {
  flex: 1 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

.chat-thread-head__presence {
  font-size: 0.72rem;
  color: var(--ink-soft);
}

.chat-thread-head__presence--on {
  color: #1f7350;
  font-weight: 600;
}

.chat-thread-head__info {
  flex-shrink: 0;
}

/* --- details / settings heads ------------------------------------------- */
.info-head {
  display: flex;
  align-items: center;
  gap: 0.85rem;
}

.info-head__text {
  min-width: 0;
  flex: 1 1;
}

.avatarupload {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-top: 0.35rem;
}

/* Your own face in the app header — the way into profile & settings, so the
   bottom bar stays at four tabs. */
.shell__me {
  display: inline-flex;
  flex-shrink: 0;
  border-radius: 50%;
}

/* --- search --------------------------------------------------------------
 * The directory's filter. Full width, 44px tall, inside the page's own
 * gutter — never edge-to-edge.
 */
.searchbox {
  font: inherit;
  width: 100%;
  min-height: 44px;
  padding: 0.55rem 0.9rem;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--card);
  color: var(--ink);
}

.searchbox:focus-visible {
  outline: 2px solid var(--teal-600);
  outline-offset: 1px;
}

.actions--spread {
  display: flex;
  gap: 0.5rem;
}

/* --- group lifecycle: archive reads as tidying, delete as consequential ---
 *
 * The two must not look like siblings. Archive is a quiet button with plain
 * copy about what SURVIVES; delete sits in its own bordered block, in the
 * error colour, and says what happens to other people's messages. Delete only
 * ever renders once a group is archived — the sequence is the safeguard, and
 * the route refuses regardless of what is on screen.
 */
.lifecycle {
  margin-top: 0.35rem;
  padding: 0.75rem 0.9rem;
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--card) 70%, var(--paper));
}

.lifecycle__title {
  margin: 0 0 0.25rem;
  font-size: 0.95rem;
  font-weight: 700;
}

.lifecycle__body {
  margin: 0 0 0.5rem;
  font-size: 0.85rem;
  color: var(--ink-soft);
}

.lifecycle--danger {
  background: var(--err-bg);
  border: 1px solid color-mix(in srgb, var(--err) 25%, transparent);
}

.lifecycle--danger .lifecycle__title,
.lifecycle--danger .lifecycle__body {
  color: var(--err);
}

/* The committed act. Solid, so it cannot be mistaken for the quiet
   secondary beside it. */
.btn--danger {
  background: var(--err);
  border-color: var(--err);
  color: #fff;
}

/* The one that only OPENS the confirmation — worded and coloured as serious,
   but not styled as the final act. */
.btn--danger-quiet {
  color: var(--err);
}

/* A quiet explanatory line under a fold's header (the Archived drawer). */
.fold__note {
  margin: 0 0 0.4rem;
  font-size: 0.78rem;
  color: var(--ink-soft);
}
=======
/* --- craft pass: groups, slider, hero numbers ----------------------------- */

.hero-stat {
  display: grid;
  grid-template-columns: 1fr auto;
  grid-gap: 0.35rem 1rem;
  gap: 0.35rem 1rem;
  align-items: end;
  padding: 1rem 1.1rem 1.15rem;
  border-radius: 18px;
  background:
    linear-gradient(135deg, color-mix(in srgb, var(--teal-700) 10%, transparent), transparent 55%),
    var(--card);
  border: 1px solid var(--line);
  box-shadow: var(--shadow);
}

.hero-stat .progress {
  grid-column: 1 / -1;
  margin: 0.35rem 0 0;
}

.hero-stat .btn {
  grid-column: 1 / -1;
  justify-self: start;
  margin-top: 0.35rem;
}

.hero-stat__label {
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.hero-stat__number {
  font-family: var(--font-display);
  font-size: clamp(2.4rem, 10vw, 3.2rem);
  font-weight: 750;
  line-height: 0.95;
  letter-spacing: -0.04em;
  color: var(--cube-gold);
  font-feature-settings: "tnum";
  font-variant-numeric: tabular-nums;
}

.hero-stat__of {
  font-size: 0.55em;
  letter-spacing: -0.02em;
  color: var(--ink-soft);
}

.record-panel {
  padding: 1rem 0 0.25rem;
  border-top: 1px solid var(--line-soft);
}

.section-title {
  font-family: var(--font-display);
  font-size: 1.15rem;
  font-weight: 700;
  letter-spacing: -0.02em;
  margin: 0;
}

.task-groups {
  display: flex;
  flex-direction: column;
  gap: 0.65rem;
}

.task-group {
  border-radius: 14px;
  background: var(--card);
  border: 1px solid var(--line);
  overflow: hidden;
}

.task-group__head {
  width: 100%;
  display: grid;
  grid-template-columns: 1fr auto auto;
  grid-gap: 0.25rem 0.65rem;
  gap: 0.25rem 0.65rem;
  align-items: center;
  padding: 0.85rem 0.95rem 0.75rem;
  background: transparent;
  border: 0;
  color: inherit;
  text-align: left;
  cursor: pointer;
  font: inherit;
  min-height: 44px;
}

.task-group__head .progress {
  grid-column: 1 / -1;
  margin: 0.15rem 0 0;
}

.task-group__title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.05rem;
  letter-spacing: -0.02em;
}

.task-group__count {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 0.95rem;
  color: var(--cube-gold);
}

.task-group__chevron {
  color: var(--ink-soft);
  font-size: 0.85rem;
}

.task-group__body {
  list-style: none;
  margin: 0;
  padding: 0 0.75rem 0.85rem;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  border-top: 1px solid var(--line-soft);
}

.task-group__admin {
  border-top: 1px solid var(--line-soft);
  border-radius: 0;
  background: transparent;
  box-shadow: none;
}

.task-item {
  padding: 0.75rem 0.15rem 0.15rem;
}

.task-item__top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
}

.task-item__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: 1.05rem;
  font-weight: 700;
  letter-spacing: -0.02em;
}

.task-item__shift {
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: #3a2c00;
  background: var(--cube-gold);
  padding: 0.15rem 0.4rem;
  border-radius: 4px;
}

.task-item__shift[data-shift="open"] {
  background: var(--cube-orange);
}

.task-item__shift[data-shift="midday"] {
  background: var(--cube-gold);
}

.task-item__shift[data-shift="close"] {
  background: var(--teal-700);
  color: #fff;
}

.task-item__meta {
  margin: 0.25rem 0 0.65rem;
  font-size: 0.82rem;
  color: var(--ink-soft);
}

.task-steps {
  list-style: none;
  margin: 0 0 0.5rem;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.65rem;
}

.task-step {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  padding: 0.55rem 0.65rem;
  border-radius: 10px;
  background: color-mix(in srgb, var(--paper) 55%, var(--card));
  border: 1px solid var(--line-soft);
}

.task-step__text {
  font-size: 0.92rem;
}

/* Three-state status slider */
.status-slider {
  display: grid;
  grid-template-columns: 1fr auto;
  grid-gap: 0.45rem 0.75rem;
  gap: 0.45rem 0.75rem;
  align-items: center;
}

.status-slider__track {
  position: relative;
  height: 2.5rem;
  border-radius: 999px;
  background: color-mix(in srgb, var(--line) 40%, var(--paper));
  border: 1px solid var(--line);
  touch-action: none;
  cursor: pointer;
  outline: none;
}

.status-slider__track:focus-visible {
  box-shadow: 0 0 0 3px rgba(255, 138, 43, 0.45);
}

.status-slider__fill {
  position: absolute;
  inset: 3px auto 3px 3px;
  border-radius: 999px;
  width: calc(0% + 1.1rem);
  transition: width 280ms cubic-bezier(0.22, 1, 0.36, 1), background 280ms ease;
  background: color-mix(in srgb, var(--line) 55%, var(--paper));
}

.status-slider--in_progress .status-slider__fill {
  width: 50%;
  background: linear-gradient(90deg, var(--cube-orange), var(--cube-gold));
}

.status-slider--done .status-slider__fill {
  width: calc(100% - 6px);
  background: linear-gradient(90deg, var(--ok), #7dffc8);
}

.status-slider__thumb {
  position: absolute;
  top: 50%;
  width: 1.55rem;
  height: 1.55rem;
  margin-left: -0.775rem;
  border-radius: 50%;
  background: #fff;
  border: 2px solid var(--teal-700);
  transform: translateY(-50%);
  transition: left 280ms cubic-bezier(0.22, 1, 0.36, 1);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
  pointer-events: none;
}

.status-slider__ticks {
  position: absolute;
  inset: 0;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0 0.55rem;
  pointer-events: none;
}

.status-slider__tick {
  width: 0.45rem;
  height: 0.45rem;
  border-radius: 50%;
  border: 0;
  padding: 0;
  background: color-mix(in srgb, var(--ink-soft) 45%, var(--paper));
  pointer-events: auto;
  cursor: pointer;
}

.status-slider__tick--active {
  background: var(--teal-700);
}

.status-slider__state {
  font-family: var(--font-display);
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  min-width: 5.5rem;
  text-align: right;
  color: var(--ink-soft);
}

.status-slider__state[data-status="in_progress"] {
  color: var(--cube-orange);
}

.status-slider__state[data-status="done"] {
  color: var(--ok);
}

.status-slider--pending {
  opacity: 0.7;
}

.badge--demo {
  background: var(--grad-cube);
  color: #1a0a12;
  border-radius: 4px;
  letter-spacing: 0.14em;
}

@media (prefers-reduced-motion: reduce) {
  .progress__fill,
  .status-slider__fill,
  .status-slider__thumb {
    transition: none !important;
  }
}

/* --- home: separate the link groups from the cards above them -------------
 * The Chat card and the Time-off row were butting straight into each other,
 * so the two read as one run of rows. A heading plus real space says where
 * the cards end and the shortcuts begin.
 */
.home-section {
  margin: 1.1rem 0 0.1rem;
  padding: 0 0.15rem;
}

.home-manage {
  margin-top: 1.1rem;
}

.home-manage .fold__summary {
  padding: 0 0.15rem;
}

/* Your own face in the header is a LINK, so the initials inherited the
   underline. The chip is the affordance; the text inside it is not a word
   you click. */
.shell__me,
.shell__me:hover,
.shell__me:focus,
.shell__me .avatar__initials,
.shell__me .avatar__img {
  text-decoration: none;
}

/* A dot under today's pill — the schedule opens here, and it should say so
   rather than leaving you to count weekdays. */
.daytab__today {
  display: block;
  width: 4px;
  height: 4px;
  margin: 2px auto 0;
  border-radius: 50%;
  background: currentColor;
}

/* A week pill states its dates. "Week 2" alone is a number nobody on a shop
   floor can place on a calendar. */
.weekpick__btn {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  line-height: 1.2;
}

.weekpick__dates {
  font-size: 0.68rem;
  font-weight: 500;
  opacity: 0.85;
}

.weekpick__now {
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  opacity: 0.9;
}

/* A call-out is always for today, so the date is a FACT on the form, not a
   field. The input stays in the DOM (it is what the action posts) but a
   person is never asked to pick a day the rule will refuse. */
.formrow--hidden {
  display: none;
}

.formrow__static {
  font-weight: 600;
  color: var(--ink);
}

/* A contradiction the SOURCE SHEET carries, not one we introduced: marked
   off for the day and also showing hours. Amber, not the red of a real OFF,
   and it carries its explanation on hover/long-press. */
.chip--conflict.chip--active {
  background: var(--warn-bg);
  color: var(--warn);
  border-color: color-mix(in srgb, var(--warn) 30%, transparent);
  cursor: help;
}

/* The copy-forward action was floating between the week nav and the day
   pills with no container, reading as a stray link. */
.actions--framed {
  padding: 0.6rem 0.9rem;
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--card) 70%, var(--paper));
  border: 1px solid var(--line-soft);
}

/* --- reordering a section of the task library ----------------------------
 * A drag handle is 44px and `touch-action: none`, which is what stops the
 * page scrolling out from under a lift on a phone. The arrows beside it do
 * the same write, so the feature is reachable without a drag at all.
 */
.reorder__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.5rem 0.9rem 0.35rem;
}

.reorder__head .page__lede {
  margin: 0;
}

.reorder {
  list-style: none;
  margin: 0;
  padding: 0 0.5rem 0.5rem;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

.reorder__row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.35rem 0.5rem;
  border-radius: 10px;
  background: var(--card);
  border: 1px solid var(--line-soft);
}

.reorder__row--lifted {
  border-color: var(--teal-700);
  box-shadow: 0 6px 18px rgba(6, 50, 44, 0.18);
}

.reorder__handle {
  font: inherit;
  font-size: 1.1rem;
  line-height: 1;
  min-width: 44px;
  min-height: 44px;
  border: none;
  border-radius: 10px;
  background: color-mix(in srgb, var(--line) 40%, var(--paper));
  color: var(--ink-soft);
  cursor: -webkit-grab;
  cursor: grab;
  touch-action: none;
}

.reorder__handle:active {
  cursor: -webkit-grabbing;
  cursor: grabbing;
}

.reorder__title {
  flex: 1 1;
  min-width: 0;
  font-size: 0.95rem;
}

.reorder__moves {
  display: flex;
  gap: 0.25rem;
}

.reorder__move {
  font: inherit;
  min-width: 40px;
  min-height: 40px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--card);
  color: var(--ink);
  cursor: pointer;
}

.reorder__move:disabled {
  opacity: 0.4;
  cursor: default;
}

/* Schedule owns the time actions now that Shift is not its own tab. Wraps on
   a narrow phone rather than shrinking below a 44px target. */
.schedule-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

/* --- week nav: the date and its marker were touching -----------------------
 * The label held "Aug 10-16, 2026" and the This-week pill in ONE span with no
 * gap, so they read as a single collided string. And the pill borrowed
 * .badge--demo, which is the loud DEMO gradient — a quiet state marker was
 * wearing the costume of a warning.
 */
.weeknav {
  gap: 0.75rem;
  padding: 0.35rem 0.15rem;
}

.weeknav__label {
  display: inline-flex;
  align-items: center;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.5rem;
  min-width: 0;
}

.weeknav__current {
  flex-shrink: 0;
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--teal-ink);
  background: var(--teal-050);
  border: 1px solid color-mix(in srgb, var(--teal-700) 22%, transparent);
  border-radius: 999px;
  padding: 0.12rem 0.5rem;
  white-space: nowrap;
}

/* The time actions were sitting straight on the Team / My schedule toggle. */
.schedule-actions {
  margin-bottom: 0.35rem;
}

/* The "not working today" opener is a row in the list, not a link adrift
   between two cards. */
.roster-rest__toggle {
  width: 100%;
  font: inherit;
  text-align: left;
  border: 1px solid var(--line-soft);
  border-radius: var(--radius);
  background: var(--card);
  cursor: pointer;
  padding-left: 1rem;
  padding-right: 1rem;
}

/* --- one spacing rule, not another one-off ---------------------------------
 * Three separate reports of the same defect (the Chat card touching the
 * shortcuts, the week date touching its marker, "Save name" touching "Your
 * details") all came from the same cause: `.page` lays its children out with
 * no gap, so a section only breathes if it happens to carry its own margin.
 * Give every top-level section after the first one real separation, once.
 */
.page > * + section {
  margin-top: 1.15rem;
}

/* A bare chevron still needs a 44px target. */
.backchevron {
  min-width: 44px;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 1.3rem;
  line-height: 1;
  padding: 0;
}

/* ---------------------------------------------------------------------------
   YOUR HOURS — the employee's own week, in money and time.
   Deliberately the loudest number on Home for anyone on the rota: one big
   figure, then the three facts that explain it. Uses the store's own teal
   rather than an alert colour — this is good news, not a warning.
   --------------------------------------------------------------------------- */
.hours {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 16px;
  padding: 1rem 1.1rem;
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
}

.hours__figure {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
}

.hours__value {
  font-size: 2.6rem;
  font-weight: 700;
  line-height: 1;
  color: var(--teal-700);
  font-feature-settings: "tnum";
  font-variant-numeric: tabular-nums;
}

.hours__unit {
  font-size: 0.95rem;
  color: var(--ink-soft);
}

.hours__facts {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  grid-gap: 0.6rem;
  gap: 0.6rem;
  margin: 0;
  padding-top: 0.75rem;
  border-top: 1px solid var(--line-soft);
}

.hours__fact {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  min-width: 0;
}

.hours__fact dt {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--ink-soft);
}

.hours__fact dd {
  margin: 0;
  font-size: 0.95rem;
  font-weight: 600;
  font-feature-settings: "tnum";
  font-variant-numeric: tabular-nums;
}

.hours__fact--total dd {
  color: var(--teal-700);
}

.hours__note {
  font-weight: 400;
  font-size: 0.75rem;
  color: var(--ink-soft);
}

.hours__caveat {
  margin: 0;
  font-size: 0.75rem;
  color: var(--ink-soft);
}

/* The pay row on Settings — a stated fact, not an editable field. */
.ratebox {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  flex-wrap: wrap;
}

.ratebox__value {
  font-size: 1.35rem;
  font-weight: 700;
  color: var(--teal-700);
  font-feature-settings: "tnum";
  font-variant-numeric: tabular-nums;
}

/* ---------------------------------------------------------------------------
   The "+" menu on the chat list. Anchored to the button, above the list, and
   dismissible by tapping anywhere else (NewChatMenu.tsx handles that half).
   --------------------------------------------------------------------------- */
.menuwrap {
  position: relative;
  display: inline-flex;
}

.menu {
  position: absolute;
  top: calc(100% + 0.4rem);
  right: 0;
  z-index: 40;
  min-width: 15rem;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow: 0 12px 30px rgba(14, 39, 35, 0.14);
  overflow: hidden;
}

.menu__item {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  padding: 0.7rem 0.9rem;
  text-decoration: none;
  color: inherit;
  background: none;
  border: 0;
  width: 100%;
  text-align: left;
  cursor: pointer;
}

.menu__item + .menu__item {
  border-top: 1px solid var(--line-soft);
}

.menu__item:hover,
.menu__item:focus-visible {
  background: color-mix(in srgb, var(--teal-700) 7%, var(--card));
}

.menu__label {
  font-weight: 600;
}

.menu__sub {
  font-size: 0.8rem;
  color: var(--ink-soft);
}

/* ---------------------------------------------------------------------------
   DANGER CONFIRM — the two-step on an irreversible-looking action. The first
   press only ARMS it; the second one does it. Deliberately not a browser
   confirm(): that dialog is muscle-memory-dismissed and says nothing about
   what is about to happen.
   --------------------------------------------------------------------------- */
.confirm {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 0.85rem;
  border: 1px solid color-mix(in srgb, #b3261e 35%, var(--line));
  border-radius: 12px;
  background: color-mix(in srgb, #b3261e 5%, var(--card));
}

.confirm__q {
  margin: 0;
  font-size: 0.9rem;
}

.confirm__row {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
}

/* ---------------------------------------------------------------------------
   PAYROLL (docs/specs/CUBOID-PAYROLL.md). Almost everything on these screens
   is the existing vocabulary — .group rows, .fold, .hours, .notice, .confirm
   — so the additions are exactly three: a button that must sit in a .group
   like the links around it (a not-yet-opened period OPENS on tap, which is a
   write, which is a button, not an <a>), a right-aligned money figure, and
   the inline "this period" chip's breathing room.
   --------------------------------------------------------------------------- */
button.group__row {
  width: 100%;
  background: none;
  border: 0;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

button.group__row:disabled {
  opacity: 0.6;
  cursor: default;
}

button.group__row:not(:disabled):active {
  background: rgba(255, 255, 255, 0.06);
}

.payroll-total {
  font-weight: 650;
  font-feature-settings: "tnum";
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.payroll-chip {
  margin-left: 0.5rem;
}

/* ---------------------------------------------------------------------------
   GUIDED TOUR (docs/specs/CUBOID-GUIDED-TOUR.md). One spotlight, one card.
   The dim IS the spotlight's box-shadow — a single element cut around the
   real UI, so there is never a seam between four dim panels. The card uses
   the app's own buttons; only the frame is new.
   --------------------------------------------------------------------------- */
.tour {
  position: fixed;
  inset: 0;
  z-index: 90;
}

.tour__spot {
  position: fixed;
  border-radius: 14px;
  box-shadow:
    0 0 0 2px var(--gold),
    0 0 0 200vmax rgba(8, 38, 33, 0.62);
  transition:
    top 180ms ease,
    left 180ms ease,
    width 180ms ease,
    height 180ms ease;
  pointer-events: none;
}

.tour__card {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  /* Docked above the tab bar, always fully on screen — the component's own
     comment says why placement never chases the spotlight. */
  bottom: calc(76px + env(safe-area-inset-bottom, 0px));
  width: min(340px, calc(100vw - 24px));
  background: var(--card);
  color: var(--ink);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 0.9rem 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

.tour__count {
  margin: 0;
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.tour__title {
  margin: 0;
  font-size: 1.05rem;
}

.tour__body {
  margin: 0;
  font-size: 0.9rem;
  color: var(--ink-soft);
  line-height: 1.45;
}

.tour__hint {
  margin: 0;
  font-size: 0.8rem;
  color: var(--ink-soft);
  font-style: italic;
}

.tour__row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.35rem;
}

.tour__spacer {
  flex: 1 1;
}

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

