/* ============================================================
   Quiver — the shell, and the default look.

   Three layers, and components may only ever name the middle one:

       palette          semantic role         component
       (a theme file)   (this file)           (this file, pmo.css)
       #007AFF     ->   --accent         ->   .q-btn, .pmo-add

   The default look is Apple's, not Cloudbreak's. Cloudbreak is a theme
   that sits on top — `theme-cloudbreak.css` — and it brings the typeface
   with it as well as the colours. Nothing in this file names a brand
   colour or a brand font.

   Rule 6 in CLAUDE.md: no hardcoded hex, font or radius outside the token
   blocks. `tests/test_theme.py` enforces it.
   ============================================================ */

:root {
  /* Tells the browser to render form controls, scrollbars and the like in
     whichever mode is active. Without it they stay light on a dark page. */
  color-scheme: light dark;

  /* ---------- backgrounds ---------- */
  /* base = the page behind everything, raised = cards and panes sitting on
     it, sunken = an inset inside a card. Apple's grouped-background idea:
     the page is the darker one and content sits above it. */
  --bg-base:   #F2F2F7;
  --bg-raised: #FFFFFF;
  --bg-sunken: #F2F2F7;
  --bg-scrim:  rgba(0, 0, 0, 0.40);

  /* ---------- labels ---------- */
  /* Four weights of text emphasis. Secondary and below are translucent on
     purpose, so they sit correctly on any background under them. */
  --label-primary:    #000000;
  --label-secondary:  rgba(60, 60, 67, 0.60);
  --label-tertiary:   rgba(60, 60, 67, 0.30);
  --label-quaternary: rgba(60, 60, 67, 0.18);
  --label-on-accent:  #FFFFFF;
  --label-on-status:  #FFFFFF;
  --label-on-highlight: #000000;

  /* ---------- fills ---------- */
  /* Control and pill backgrounds. Grey and translucent so they read the
     same on white, on the page, and in dark mode. */
  --fill-primary:    rgba(120, 120, 128, 0.20);
  --fill-secondary:  rgba(120, 120, 128, 0.16);
  --fill-tertiary:   rgba(120, 120, 128, 0.12);
  --fill-quaternary: rgba(120, 120, 128, 0.08);

  /* ---------- separators ---------- */
  /* The hairline is translucent so it darkens whatever it crosses; the
     opaque one is for when something must not show through. */
  --separator:        rgba(60, 60, 67, 0.29);
  --separator-opaque: #C6C6C8;

  /* ---------- accent ---------- */
  /* The one tint colour. This is where a brand actually shows. */
  --accent:         #007AFF;
  --accent-pressed: #0062CC;

  /* Gridiron's two lenses (QVR-363): Owner is the accent, Commissioner is
     purple, his call on the 20 Oct drawing, so the two never look alike. */
  --lens-owner:        var(--accent);
  --lens-commissioner: var(--category-4);

  /* ---------- status ---------- */
  /* Meaning, not decoration. A theme may recolour these but must not drop
     one: a screen that cannot say "blocked" in red has lost information.
     The tints are derived, so a theme only ever sets the solid colour. */
  --status-danger:  #FF3B30;
  --status-warning: #FF9500;
  --status-success: #34C759;
  --status-info:    #007AFF;
  --highlight:      #FFCC00;

  /* Pressing darkens in the light and lightens in the dark, so a theme gets
     it for free by setting the solid colour alone. */
  --status-danger-pressed: color-mix(in srgb, var(--status-danger) 85%, black);

  --status-danger-tint:  color-mix(in srgb, var(--status-danger) 12%, var(--bg-raised));
  --status-warning-tint: color-mix(in srgb, var(--status-warning) 14%, var(--bg-raised));
  --status-success-tint: color-mix(in srgb, var(--status-success) 14%, var(--bg-raised));
  --status-info-tint:    color-mix(in srgb, var(--status-info) 10%, var(--bg-raised));
  --highlight-tint:      color-mix(in srgb, var(--highlight) 26%, var(--bg-raised));
  --highlight-wash:      color-mix(in srgb, var(--highlight) 22%, transparent);

  /* ---------- categorical accents ---------- */
  /* For telling things apart, not for saying anything. A program picks one
     to be recognised by; the slot carries the identity and the theme decides
     the hue.

     Ten, not five. It was five because that is what the program model
     offered; Commish's season chart draws one line per franchise and the
     league has ten, so a ten-team chart either gets ten slots or starts
     repeating a colour — and two identical lines on a chart people are meant
     to find themselves on is worse than no chart. A theme must define all
     ten, which is the honest cost of widening a role rather than inventing a
     second palette beside it. */
  --category-1: #007AFF;
  --category-2: #FF9500;
  --category-3: #34C759;
  --category-4: #AF52DE;
  --category-5: #FF2D55;
  --category-6: #5AC8FA;
  --category-7: #FFCC00;
  --category-8: #00C7BE;
  --category-9: #A2845E;
  --category-10: #8E8E93;

  /* The red line across today's timeline. Apple's calendars use systemRed
     for this and it is not a danger state, so it gets its own name. */
  --now-indicator: #FF3B30;

  /* ---------- sidebar ---------- */
  /* Roles, not a second palette. A macOS sidebar is a quiet inset panel
     with an accent-filled selected row. */
  --side-bg:           var(--bg-sunken);
  --side-text:         var(--label-primary);
  --side-muted:        var(--label-secondary);
  --side-hover:        var(--fill-quaternary);
  --side-active:       var(--accent);
  --side-active-text:  var(--label-on-accent);
  --side-separator:    var(--separator);

  /* ---------- the cap on a pane ---------- */
  /* Cloudbreak caps its panes with the sunset stripe. The default look has
     no such ornament, so the height is zero and the band never paints.
     Kept as tokens rather than as a theme-only rule, so the theme changes
     a value instead of re-stating the component. */
  --pane-cap-height: 0px;
  --pane-cap: transparent;

  /* ---------- type ---------- */
  /* The family is a token because the Cloudbreak theme changes it. Varela
     Round belongs to the brand exactly as the colours do. */
  --font-ui: -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui,
             "Segoe UI", Roboto, sans-serif;
  /* For a key or an address to copy exactly (the side button, 2 Oct). */
  --font-mono: ui-monospace, "SF Mono", Menlo, monospace;

  /* Apple's Dynamic Type scale, in rem so the phone's text-size setting
     works. 1rem = 16px unless the reader has changed it. */
  --text-display: 2.125rem;  /* 34 — large title */
  --text-h1:      1.75rem;   /* 28 — title 1 */
  --text-h2:      1.375rem;  /* 22 — title 2 */
  --text-h3:      1.0625rem; /* 17 — headline */
  --text-body:    1.0625rem; /* 17 — body */
  --text-small:   0.8125rem; /* 13 — footnote */
  --text-caption: 0.6875rem; /* 11 — caption 2 */

  --leading-tight: 1.2;
  --leading-body:  1.45;

  --tracking-wordmark: 0.14em;
  --tracking-label:    0.22em;

  /* ---------- shape ---------- */
  --radius-tile: 8px;
  --radius-card: 12px;
  --radius-pill: 999px;
  /* An app icon on Home: a rounded square, like a phone's home screen. */
  --radius-app-icon: 22%;

  --shadow-card:   0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 1px rgba(0, 0, 0, 0.04);
  --shadow-raised: 0 1px 2px rgba(0, 0, 0, 0.10);

  /* ---------- layout ---------- */
  --sidebar-w: 238px;
  /* The collapsed sidebar: the icons alone, still one click to each. */
  --sidebar-strip-w: 64px;
  --tabbar-h: 52px;
  /* 44pt is Apple's minimum touch target, and the reason it is a token is
     so a control can say what it is rather than repeat the number. */
  --touch-min: 44px;
}

/* ---------- dark mode ---------- */
/* Only the semantic layer changes. If a component needs to know which mode
   it is in, that component is wrong. */

@media (prefers-color-scheme: dark) {
  :root {
    --bg-base:   #1C1C1E;
    --bg-raised: #2C2C2E;
    --bg-sunken: #3A3A3C;
    --bg-scrim:  rgba(0, 0, 0, 0.60);

    --label-primary:    #FFFFFF;
    --label-secondary:  rgba(235, 235, 245, 0.60);
    --label-tertiary:   rgba(235, 235, 245, 0.30);
    --label-quaternary: rgba(235, 235, 245, 0.18);

    --fill-primary:    rgba(120, 120, 128, 0.36);
    --fill-secondary:  rgba(120, 120, 128, 0.32);
    --fill-tertiary:   rgba(120, 120, 128, 0.24);
    --fill-quaternary: rgba(120, 120, 128, 0.18);

    --separator:        rgba(84, 84, 88, 0.65);
    --separator-opaque: #48484A;

    --accent:         #0A84FF;
    --accent-pressed: #409CFF;

    --status-danger:  #FF453A;
    --status-warning: #FF9F0A;
    --status-success: #30D158;
    --status-info:    #0A84FF;
    --highlight:      #FFD60A;

    --now-indicator: #FF453A;

    --category-1: #0A84FF;
    --category-2: #FF9F0A;
    --category-3: #30D158;
    --category-4: #BF5AF2;
    --category-5: #FF375F;
    --category-6: #64D2FF;
    --category-7: #FFD60A;
    --category-8: #66D4CF;
    --category-9: #AC8E68;
    --category-10: #98989D;

    --status-danger-pressed: color-mix(in srgb, var(--status-danger) 85%, white);

    /* A wash that reads as a gentle tint on white turns muddy on a dark
       ground — 26% yellow over #2C2C2E is olive. Dark mode needs less of
       the colour, not the same amount. */
    --status-danger-tint:  color-mix(in srgb, var(--status-danger) 18%, var(--bg-raised));
    --status-warning-tint: color-mix(in srgb, var(--status-warning) 18%, var(--bg-raised));
    --status-success-tint: color-mix(in srgb, var(--status-success) 18%, var(--bg-raised));
    --status-info-tint:    color-mix(in srgb, var(--status-info) 20%, var(--bg-raised));
    --highlight-tint:      color-mix(in srgb, var(--highlight) 8%, var(--bg-raised));
    --highlight-wash:      color-mix(in srgb, var(--highlight) 12%, transparent);

    --shadow-card:   0 1px 3px rgba(0, 0, 0, 0.50);
    --shadow-raised: 0 1px 2px rgba(0, 0, 0, 0.60);
  }
}

/* ---------- reset ---------- */

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

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
}

/* No double-tap zoom, anywhere. Two quick taps on a button (Confirm, in
   Sort, dozens of times in a row) were zooming the page on iOS, and a zoomed
   page strands the fixed tab bar mid-screen and cuts off the right edge —
   the "completely broken" bar of 27 Sep 2026. Pinch zoom still works.
   Blue Lobsters has done this for its own buttons since session 10. */
html { touch-action: manipulation; }

/* An app's own bottom bar exists only on a phone; see the 820px block. */
.q-appbar { display: none; }

body {
  font-family: var(--font-ui);
  font-size: var(--text-body);
  line-height: var(--leading-body);
  color: var(--label-primary);
  background: var(--bg-base);
  -webkit-font-smoothing: antialiased;
}

a { color: inherit; text-decoration: none; }

/* Alpine is the hub's (vendored in `hub/static/vendor/`), so the rule that
   hides a component until it has been initialised belongs here too. Only
   `pmo.css` had it, which is why every other spoke flashed its raw bindings
   for a frame on load. */
[x-cloak] { display: none !important; }

/* Visible keyboard focus everywhere — do not remove */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}

/* ---------- app shell ---------- */

.q-app {
  display: flex;
  min-height: 100vh;
}

.q-sidebar {
  width: var(--sidebar-w);
  flex: 0 0 var(--sidebar-w);
  background: var(--side-bg);
  color: var(--side-text);
  border-right: 1px solid var(--side-separator);
  display: flex;
  flex-direction: column;
  padding: 20px 14px 16px;
  position: sticky;
  top: 0;
  height: 100vh;
}

.q-sidebar__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 6px;
  padding: 0 0 22px 6px;
}

.q-brand {
  display: flex;
  align-items: center;
  gap: 12px;
  min-width: 0;
}

/* A plain icon button — the browser's grey fill and border would make it the
   loudest thing in the sidebar (session 11 learned that about <button>). */
.q-sidebar__toggle {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  padding: 0;
  border: none;
  border-radius: var(--radius-tile);
  background: none;
  color: var(--side-muted);
  cursor: pointer;
}

.q-sidebar__toggle:hover { background: var(--side-hover); color: var(--side-text); }

/* Quiver's own mark (9 Oct 2026), the same square as its Home Screen icon. */
.q-brand__mark {
  display: block;
  width: 34px;
  height: 34px;
  border-radius: var(--radius-tile);
  flex: 0 0 34px;
}

.q-brand__name {
  font-size: var(--text-h3);
  font-weight: 600;
  letter-spacing: normal;
  line-height: 1.1;
}

.q-brand__tag {
  font-size: var(--text-caption);
  color: var(--side-muted);
}

.q-nav {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1;
}

.q-nav__item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px 12px;
  min-height: var(--touch-min);
  border-radius: var(--radius-tile);
  color: var(--side-text);
  font-size: var(--text-h3);
  cursor: pointer;
  transition: background 120ms ease;
}

.q-nav__item:hover { background: var(--side-hover); }

/* A line put away in Settings. `display: flex` above outranks what the
   `hidden` attribute asks for, so without this the attribute does nothing —
   the row would still be drawn and the setting would look broken. */
.q-nav__item[hidden] { display: none; }

.q-nav__item.is-active {
  background: var(--side-active);
  color: var(--side-active-text);
}

/* --side-muted, not --label-tertiary. The sidebar has its own text roles
   because a theme may put it on a dark ground: Cloudbreak's sidebar is navy,
   and a label role mixed from navy is invisible on it. A spoke that is not
   built yet has to stay readable, or it looks like it went missing. */
/* An app's own pages, under its line (an app with a phone bar, on a laptop). */
.q-nav__item--sub { padding-left: 32px; font-size: var(--text-small); }
.q-nav__item--sub.is-current { color: var(--side-active); font-weight: 600; }

.q-nav__item[aria-disabled="true"] {
  color: var(--side-muted);
  cursor: default;
}
.q-nav__item[aria-disabled="true"]:hover { background: none; }

.q-sidebar__foot {
  border-top: 1px solid var(--side-separator);
  padding-top: 14px;
}

/* The whole row is the button that opens Settings, so the target is the name
   and the avatar rather than a word beside them. It is transparent and
   inherits its colour: a <button> keeps the browser's grey fill and its own
   font until told otherwise, which is how the quietest control on the budget
   spoke's swipe screen came out looking like the loudest (session 11). */
.q-account {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 6px;
  margin: -6px;
  border: none;
  border-radius: var(--radius-tile);
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.q-account:hover { background: var(--side-hover); }

.q-user { min-width: 0; }

.q-avatar {
  width: 28px;
  height: 28px;
  border-radius: var(--radius-pill);
  background: var(--accent);
  color: var(--label-on-accent);
  display: grid;
  place-items: center;
  font-size: var(--text-small);
  flex: 0 0 28px;
}

.q-user__name {
  display: block;
  font-size: var(--text-small);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.q-user__role {
  display: block;
  font-size: var(--text-caption);
  color: var(--side-muted);
}

/* ---------- the collapsed sidebar ---------- */
/* An icon strip, on a desktop only. The phone's sidebar is the bottom tab
   bar and has nothing to collapse, so every rule here sits behind the same
   820px line the tab bar does — a cookie set on the laptop must not reach
   into the phone layout. */

/* Hidden from the eye, not from a screen reader. */
.q-nav__label { white-space: nowrap; }

@media (min-width: 821px) {
  .q-app.is-collapsed .q-sidebar {
    width: var(--sidebar-strip-w);
    flex-basis: var(--sidebar-strip-w);
    padding-left: 10px;
    padding-right: 10px;
    /* The hover labels hang out past the strip, over the page. */
    z-index: 10;
  }

  .q-app.is-collapsed .q-sidebar__head { justify-content: center; padding-left: 0; }
  .q-app.is-collapsed .q-brand { display: none; }

  .q-app.is-collapsed .q-nav__item {
    justify-content: center;
    padding: 8px 0;
    position: relative;
  }

  .q-app.is-collapsed .q-nav__label {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
  }

  /* The label the strip gave up, shown on hover and on keyboard focus. */
  .q-app.is-collapsed .q-nav__item:hover::after,
  .q-app.is-collapsed .q-nav__item:focus-visible::after {
    content: attr(data-tip);
    position: absolute;
    left: calc(100% + 10px);
    top: 50%;
    transform: translateY(-50%);
    padding: 5px 10px;
    border-radius: var(--radius-tile);
    background: var(--label-primary);
    color: var(--bg-raised);
    font-size: var(--text-small);
    white-space: nowrap;
    box-shadow: var(--shadow-raised);
    pointer-events: none;
  }

  .q-app.is-collapsed .q-user { display: none; }

  .q-app.is-collapsed .q-account {
    justify-content: center;
    position: relative;
  }

  /* The label the strip gave up, same as a nav item's. */
  .q-app.is-collapsed .q-account:hover::after,
  .q-app.is-collapsed .q-account:focus-visible::after {
    content: attr(data-tip);
    position: absolute;
    left: calc(100% + 10px);
    top: 50%;
    transform: translateY(-50%);
    padding: 5px 10px;
    border-radius: var(--radius-tile);
    background: var(--label-primary);
    color: var(--bg-raised);
    font-size: var(--text-small);
    white-space: nowrap;
    box-shadow: var(--shadow-raised);
    pointer-events: none;
  }
}

/* ---------- main ---------- */

.q-main {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

.q-topbar {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 18px 26px;
}

/* The ‹ back to Home: the phone's way out (2 Oct, his 2A). On a laptop the
   sidebar is right there, so the name alone is the link. */
.q-topbar__back { display: none; color: var(--accent); }

/* An app's own settings: a small, quiet button top right, out of its bar
   (2 Oct, his 5: "extra hidden nav"). */
.q-topbar__settings {
  position: relative;
  margin-left: auto;
  display: grid;
  place-items: center;
  width: var(--touch-min);
  height: var(--touch-min);
  border-radius: var(--radius-pill);
  color: var(--label-secondary);
}

/* An app's small menu beside its settings (Commish's league): it takes the
   push to the right, and the settings button sits next to it. */
.q-topbar__menu { margin-left: auto; position: relative; }
.q-topbar__menu + .q-topbar__settings { margin-left: 0; }

.q-topbar__settings:hover { background: var(--fill-quaternary); }
.q-topbar__settings.is-active { color: var(--accent); }

/* Something there wants him (Commish's missing waiver day). */
.q-topbar__dot {
  position: absolute;
  top: 9px;
  right: 9px;
  width: 8px;
  height: 8px;
  border-radius: var(--radius-pill);
  background: var(--status-danger);
}

/* An app's pages as tabs along the top, on a laptop (2 Oct, Commish). The
   phone has the app's own bottom bar instead, so the row goes there. */
.q-apptabs {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: -6px 26px 14px;
}

.q-apptabs__item {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 14px;
  border-radius: var(--radius-pill);
  color: var(--label-secondary);
  font-size: var(--text-small);
  font-weight: 500;
}

.q-apptabs__item:hover {
  background: var(--fill-quaternary);
  color: var(--label-primary);
}

.q-apptabs__item.is-active {
  background: var(--accent);
  color: var(--label-on-accent);
}

/* A spoke's own mark, where it has one (Single Fin): a picture, not a
   glyph, so it fills its box and keeps its own colours. */
.q-mark { display: block; border-radius: var(--radius-tile); }
.q-topbar__icon:has(.q-mark) { background: none; border: 0; overflow: hidden; }

/* An app with its own bar: the mark and name are one link back to Quiver. */
.q-topbar__home {
  display: flex;
  align-items: center;
  gap: inherit;
  min-width: 0;
  color: inherit;
  text-decoration: none;
}

.q-topbar__icon {
  width: 38px;
  height: 38px;
  border-radius: var(--radius-tile);
  background: var(--bg-raised);
  border: 1px solid var(--separator);
  display: grid;
  place-items: center;
  flex: 0 0 38px;
}

.q-topbar__title {
  font-size: var(--text-h2);
  font-weight: 600;
  line-height: var(--leading-tight);
  margin: 0;
}

.q-topbar__desc {
  font-size: var(--text-small);
  color: var(--label-secondary);
  margin: 0;
}

/* A spoke with more than one screen links across from here — `margin-left:
   auto` pushes it to the far end so it never crowds the title. Session 14's
   first use is Coaching, which has three. */
.q-topbar__actions {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 0 0 auto;
}

.q-topbar__link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 8px 14px;
  border-radius: var(--radius-tile);
  border: 1px solid var(--separator);
  background: var(--bg-raised);
  color: var(--label-primary);
  font-size: var(--text-small);
  font-weight: 500;
  text-decoration: none;
}

.q-topbar__link:hover { background: var(--fill-quaternary); }

.q-pane {
  flex: 1;
  margin: 0 26px 26px;
  background: var(--bg-raised);
  border: 1px solid var(--separator);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-card);
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

/* Cloudbreak caps the pane with the sunset stripe. In the default look the
   height is zero, so this paints nothing at all. */
.q-pane::before {
  content: "";
  display: block;
  height: var(--pane-cap-height);
  background: var(--pane-cap);
  flex: 0 0 var(--pane-cap-height);
}

.q-pane__body { flex: 1; padding: 26px; }

/* ---------- home: the app icons ---------- */
/* QVR-368, his 1B (24 Oct 2026): a square icon per app, a red dot when it
   needs him, nothing else. Four across on a laptop, two on a phone. */

.q-home { display: flex; flex-direction: column; }

.q-home__grid {
  display: grid;
  grid-template-columns: repeat(4, 104px);
  justify-content: center;
  gap: 28px 32px;
  padding: 28px 0;
}

@media (max-width: 600px) {
  .q-home__grid { grid-template-columns: repeat(2, 104px); gap: 26px 40px; padding-top: 12px; }
}

.q-app-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  min-width: 0;
  color: var(--label-primary);
  text-decoration: none;
  -webkit-tap-highlight-color: transparent;
}

.q-app-tile[hidden] { display: none; }

.q-app-tile__icon {
  position: relative;
  width: 84px;
  height: 84px;
  border-radius: var(--radius-app-icon);
  background: var(--tile-tint, var(--accent));
  color: var(--label-on-accent);
  display: grid;
  place-items: center;
  box-shadow: 0 1px 2px color-mix(in srgb, var(--label-primary) 18%, transparent);
  transition: transform 120ms ease;
}

/* An app with a mark of its own (Gridiron, Single Fin) wears it whole. */
.q-app-tile__icon--mark { background: none; }
.q-app-tile__icon--mark .q-mark { width: 100%; height: 100%; border-radius: var(--radius-app-icon); }

.q-app-tile:hover .q-app-tile__icon { transform: translateY(-2px); }
.q-app-tile:active .q-app-tile__icon { transform: scale(0.96); }
.q-app-tile:focus-visible { outline: none; }
.q-app-tile:focus-visible .q-app-tile__icon { outline: 3px solid var(--accent); outline-offset: 3px; }

.q-app-tile__name {
  font-size: var(--text-small);
  font-weight: 600;
  text-align: center;
  line-height: var(--leading-tight);
  overflow-wrap: anywhere;
}

/* The one thing an icon says: this app needs you today. */
.q-app-tile__dot {
  position: absolute;
  top: -6px;
  right: -6px;
  width: 20px;
  height: 20px;
  border-radius: var(--radius-pill);
  background: var(--status-danger);
  border: 3px solid var(--bg-base);
}

.q-app-tile__dot[hidden] { display: none; }

/* Moving the icons (his A): hold, they wiggle, drag, Done. */
.q-app-tile { -webkit-touch-callout: none; user-select: none; -webkit-user-select: none; }
.q-app-tile img { -webkit-user-drag: none; pointer-events: none; }

.is-arranging .q-app-tile { touch-action: none; cursor: grab; }
.is-arranging .q-app-tile__icon { animation: q-wiggle 0.28s ease-in-out infinite alternate; }
.is-arranging .q-app-tile:nth-child(even) .q-app-tile__icon { animation-direction: alternate-reverse; }
.is-arranging .q-app-tile__dot { display: none; }

.q-app-tile.is-dragging { position: relative; z-index: 2; cursor: grabbing; }
.q-app-tile.is-dragging .q-app-tile__icon { animation: none; transform: scale(1.1); box-shadow: 0 8px 20px color-mix(in srgb, var(--label-primary) 25%, transparent); }

@keyframes q-wiggle {
  from { transform: rotate(-2.5deg); }
  to   { transform: rotate(2.5deg); }
}

@media (prefers-reduced-motion: reduce) {
  .is-arranging .q-app-tile__icon { animation: none; outline: 2px dashed var(--label-tertiary); outline-offset: 4px; }
}

.q-home__done {
  margin-left: auto;
  min-height: 34px;
  padding: 0 16px;
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--accent);
  color: var(--label-on-accent);
  font: inherit;
  font-weight: 700;
  cursor: pointer;
}

.q-home__done[hidden],
.q-topbar__gear[hidden] { display: none; }

/* The gear (his 5A): quiet, top right of Home. */
.q-topbar__gear {
  margin-left: auto;
  display: grid;
  place-items: center;
  width: var(--touch-min);
  height: var(--touch-min);
  border: 0;
  background: none;
  color: var(--label-secondary);
  cursor: pointer;
}

.q-app--home .q-topbar__icon { display: none; }

/* ---------- the sunrise: Quiver's opening screen ---------- */
/* QVR-368, approved 24 Oct 2026. The colours are theme-sunrise.css's; the
   timing is sunrise.js's, which sets data-ray from 0 to 5 as the rays light.
   The sun, the lit rays and the sky all step up together; on the fifth
   everything is at full and the sun glows. */

.q-sunrise {
  position: fixed;
  inset: 0;
  z-index: 1000;
  overflow: hidden;
  background: var(--sunrise-sea);
  transition: opacity 400ms ease;
}

.q-sunrise[hidden] { display: none; }
.q-sunrise.is-leaving { opacity: 0; }

/* The way out if sunrise.js never runs (a failed load, an old cache): the
   screen steps aside by itself after twelve seconds, script or no script,
   so it can never cover the page for good. */
.q-sunrise { animation: q-sunrise-failsafe 0.4s ease 12s forwards; }

@keyframes q-sunrise-failsafe {
  to { opacity: 0; visibility: hidden; pointer-events: none; }
}

/* The sky down to the horizon, which the sun sits on. */
.q-sunrise__sky {
  position: absolute;
  inset: 0 0 auto;
  height: calc(env(safe-area-inset-top, 0px) + 18vh);
  border-bottom: 1.5px solid color-mix(in srgb, var(--sunrise-cream) 55%, transparent);
  background: var(--sunrise-sky-0);
  transition: background-color 300ms ease;
}

.q-sunrise__sun {
  position: absolute;
  left: 50%;
  top: calc(env(safe-area-inset-top, 0px) + 18vh);
  width: min(30vw, 150px);
  aspect-ratio: 58 / 34;
  transform: translate(-50%, -96%);
  overflow: visible;
  transition: filter 300ms ease;
}

.q-sunrise__disc { fill: var(--sunrise-cream); transition: opacity 300ms ease; }
.q-sunrise__ray {
  fill: none;
  stroke: var(--sunrise-cream);
  stroke-width: 2.6;
  stroke-linecap: round;
  opacity: 0.08;
  transition: opacity 300ms ease;
}

.q-sunrise[data-ray="5"] .q-sunrise__sun { filter: drop-shadow(0 0 12px var(--sunrise-glow)); }

.q-sunrise[data-ray="0"] .q-sunrise__sky { background: var(--sunrise-sky-0); }
.q-sunrise[data-ray="0"] .q-sunrise__disc { opacity: 0.3; }
.q-sunrise[data-ray="1"] .q-sunrise__sky { background: var(--sunrise-sky-1); }
.q-sunrise[data-ray="1"] .q-sunrise__disc { opacity: 0.4; }
.q-sunrise[data-ray="1"] [data-ray-n="1"] { opacity: 0.4; }
.q-sunrise[data-ray="2"] .q-sunrise__sky { background: var(--sunrise-sky-2); }
.q-sunrise[data-ray="2"] .q-sunrise__disc { opacity: 0.5; }
.q-sunrise[data-ray="2"] [data-ray-n="1"], .q-sunrise[data-ray="2"] [data-ray-n="2"] { opacity: 0.5; }
.q-sunrise[data-ray="3"] .q-sunrise__sky { background: var(--sunrise-sky-3); }
.q-sunrise[data-ray="3"] .q-sunrise__disc { opacity: 0.6; }
.q-sunrise[data-ray="3"] [data-ray-n="1"], .q-sunrise[data-ray="3"] [data-ray-n="2"], .q-sunrise[data-ray="3"] [data-ray-n="3"] { opacity: 0.6; }
.q-sunrise[data-ray="4"] .q-sunrise__sky { background: var(--sunrise-sky-4); }
.q-sunrise[data-ray="4"] .q-sunrise__disc { opacity: 0.7; }
.q-sunrise[data-ray="4"] [data-ray-n="1"], .q-sunrise[data-ray="4"] [data-ray-n="2"], .q-sunrise[data-ray="4"] [data-ray-n="3"], .q-sunrise[data-ray="4"] [data-ray-n="4"] { opacity: 0.7; }
.q-sunrise[data-ray="5"] .q-sunrise__sky { background: var(--sunrise-sky-5); }
.q-sunrise[data-ray="5"] .q-sunrise__disc { opacity: 1; }
.q-sunrise[data-ray="5"] [data-ray-n="1"], .q-sunrise[data-ray="5"] [data-ray-n="2"], .q-sunrise[data-ray="5"] [data-ray-n="3"], .q-sunrise[data-ray="5"] [data-ray-n="4"], .q-sunrise[data-ray="5"] [data-ray-n="5"] { opacity: 1; }

/* The Cloudbreak mark's waves, rolling in slowly (his: not too fast). */
.q-sunrise__waves {
  position: absolute;
  left: 0;
  right: 0;
  top: calc(env(safe-area-inset-top, 0px) + 18vh + 14px);
  bottom: 12%;
}

.q-sunrise__wave {
  position: absolute;
  left: 50%;
  height: 9px;
  overflow: visible;
  width: calc(30% + var(--wave, 0) * 17%);
  top: calc(6% + var(--wave, 0) * 17%);
  transform: translateX(-50%);
  opacity: calc(0.12 + var(--wave, 0) * 0.06);
  animation: q-sunrise-wave 9s linear infinite;
  animation-delay: calc(var(--wave, 0) * -1.8s);
}

.q-sunrise__wave path { fill: none; stroke: var(--sunrise-cream); stroke-width: 2.2; stroke-linecap: round; }

@keyframes q-sunrise-wave {
  0%   { top: 0%;  width: 20%;  opacity: 0; }
  15%  { opacity: 0.12; }
  100% { top: 94%; width: 110%; opacity: 0.35; }
}

/* Sand along the bottom, and the boards rising out of it. */
.q-sunrise__sand {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  width: 100%;
  height: calc(env(safe-area-inset-bottom, 0px) + 13vh);
  z-index: 1;
}

.q-sunrise__beach { fill: var(--sunrise-sand); }
.q-sunrise__shore { fill: none; stroke: var(--sunrise-cream); stroke-width: 1.6; opacity: 0.8; }

.q-sunrise__rise {
  position: absolute;
  left: 50%;
  bottom: 0;
  z-index: 2;
  width: min(42vw, 220px);
  transform: translate(-50%, 85%);
}

.q-sunrise.is-playing .q-sunrise__rise { animation: q-sunrise-rise 1.8s cubic-bezier(0.2, 0.8, 0.2, 1) 0.15s forwards; }

@keyframes q-sunrise-rise {
  from { transform: translate(-50%, 85%); }
  to   { transform: translate(-50%, 0); }
}

.q-sunrise__boards { display: block; width: 100%; aspect-ratio: 32 / 36; }
.q-sunrise__board { fill: var(--sunrise-cream); }
.q-sunrise__stringer { stroke: var(--sunrise-sea); stroke-width: 0.7; }

@media (prefers-reduced-motion: reduce) {
  .q-sunrise__wave { animation: none; }
  .q-sunrise.is-playing .q-sunrise__rise { animation: none; transform: translate(-50%, 0); }
}

/* ---------- empty state ---------- */

.q-empty {
  height: 100%;
  min-height: 320px;
  display: grid;
  place-content: center;
  justify-items: center;
  gap: 8px;
  text-align: center;
  color: var(--label-secondary);
}

.q-empty__title { font-size: var(--text-h3); font-weight: 600; color: var(--label-primary); }
.q-empty__hint  { font-size: var(--text-small); max-width: 42ch; }

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

.q-login__page { background: var(--bg-base); }

.q-login {
  min-height: 100vh;
  display: grid;
  place-content: center;
  padding: 24px;
}

.q-login__card {
  width: min(380px, 100%);
  background: var(--bg-raised);
  border: 1px solid var(--separator);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-card);
  padding: 30px 26px 26px;
  overflow: hidden;
  position: relative;
}

/* The same cap as the content pane — nothing in the default look. */
.q-login__card::before {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  height: var(--pane-cap-height);
  background: var(--pane-cap);
}

.q-login__brand { padding: 0 0 20px; }

.q-login__brand .q-brand__tag { color: var(--label-secondary); }

.q-login__hint {
  font-size: var(--text-small);
  color: var(--label-secondary);
  margin: 0 0 20px;
}

.q-privacy { width: min(560px, calc(100vw - 48px)); }

.q-privacy p {
  font-size: var(--text-small);
  color: var(--label-secondary);
  margin: 0 0 16px;
}

.q-privacy h2 {
  font-size: var(--text-body);
  font-weight: 600;
  color: var(--label-primary);
  margin: 20px 0 6px;
}

.q-login__error {
  font-size: var(--text-small);
  color: var(--status-danger);
  border: 1px solid var(--status-danger);
  background: var(--status-danger-tint);
  border-radius: var(--radius-tile);
  padding: 10px 12px;
  margin: 0 0 18px;
}

.q-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  width: 100%;
  min-height: var(--touch-min);
  padding: 11px 16px;
  border: none;
  border-radius: var(--radius-tile);
  background: var(--accent);
  color: var(--label-on-accent);
  font-family: var(--font-ui);
  font-size: var(--text-h3);
  cursor: pointer;
  transition: background 120ms ease;
}

.q-btn:hover { background: var(--accent-pressed); }

/* ---------- the settings sheet ---------- */
/* Opened from the account row, on every page, because a setting you have to
   navigate to is a setting you change less often than you want to. The
   markup is rendered by the server and hidden, so it opens on the first
   frame with the right answers in it and asks the server nothing.

   The scrim is the click target for closing, and Escape closes it too: a
   modal with one way out is a modal somebody gets stuck in. */

.q-sheet {
  position: fixed;
  inset: 0;
  z-index: 80;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  background: var(--bg-scrim);
}

.q-sheet[hidden] { display: none; }

.q-sheet__panel {
  width: 100%;
  max-width: 420px;
  max-height: 88vh;
  overflow: auto;
  padding: 22px;
  border-radius: var(--radius-card);
  background: var(--bg-raised);
  box-shadow: var(--shadow-card);
  display: flex;
  flex-direction: column;
  gap: 18px;
}

.q-sheet__title { margin: 0; font-size: var(--text-h2); }

.q-sheet__section {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.q-sheet__heading {
  margin: 0;
  font-size: var(--text-caption);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--label-secondary);
}

.q-sheet__hint {
  margin: 0;
  font-size: var(--text-small);
  color: var(--label-secondary);
}

/* A refused save has already been undone on screen, so this never blocks
   anything — it says what the server refused and nothing more. */
.q-sheet__error {
  margin: 0;
  font-size: var(--text-small);
  color: var(--status-danger);
}

.q-sheet__error[hidden] { display: none; }

.q-sheet__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

.q-sheet__foot { justify-content: flex-end; }
.q-sheet__foot .q-btn { width: auto; }

/* A link to another settings page, named for its app. */
.q-sheet__item {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: 10px;
  align-items: center;
  min-height: var(--touch-min);
  padding: 8px 12px;
  border-radius: var(--radius-tile);
  background: var(--fill-quaternary);
  color: var(--label-primary);
  text-decoration: none;
}

/* Sign out is a way out, not the thing this sheet is for: last, small,
   muted, under a dashed line (his 5A). */
.q-sheet__signout {
  display: block;
  padding-top: 12px;
  border-top: 1px dashed var(--separator);
  text-align: center;
  font-size: var(--text-small);
  color: var(--label-secondary);
  text-decoration: none;
}

/* ---- one row per app ---- */

.q-toggles {
  list-style: none;
  margin: 4px 0 0;
  padding: 0;
}

.q-toggle + .q-toggle { border-top: 1px solid var(--separator); }

.q-toggle__label {
  display: flex;
  align-items: center;
  gap: 12px;
  /* A row of an app list is a touch target even on a desktop: it is a
     checkbox, a glyph and a word, and missing it toggles the wrong app. */
  min-height: var(--touch-min);
  cursor: pointer;
}

.q-toggle__box {
  width: 18px;
  height: 18px;
  flex: 0 0 18px;
  accent-color: var(--accent);
  cursor: pointer;
}

.q-toggle__icon {
  display: inline-flex;
  color: var(--label-secondary);
}

.q-toggle__name { font-size: var(--text-body); }

/* ---------- the phone ---------- */
/* The sidebar is a desktop shape. On a phone the navigation belongs at the
   bottom, in thumb reach, which is a different element rather than the same
   one reflowed — so the sidebar's chrome is hidden and .q-nav becomes a
   fixed tab bar. */

@media (max-width: 820px) {
  .q-apptabs { display: none; }

  .q-app { flex-direction: column; }

  .q-sidebar {
    width: 100%;
    flex: none;
    height: auto;
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    top: auto;
    z-index: 20;
    padding: 0;
    border-right: none;
    border-top: 1px solid var(--side-separator);
    /* Sit above the home indicator rather than under it. */
    padding-bottom: env(safe-area-inset-bottom);
    background: var(--bg-raised);
  }

  /* The wordmark and the account row are desktop sidebar furniture. On a
     phone the tab bar is the whole sidebar. */
  .q-sidebar__head,
  .q-sidebar__foot { display: none; }

  .q-nav {
    flex-direction: row;
    justify-content: space-around;
    align-items: stretch;
    gap: 0;
    flex: none;
    min-height: var(--tabbar-h);
  }

  .q-nav__item {
    flex: 1 1 0;
    /* A flex item will not shrink below its content without this, so seven
       nowrap labels made the fixed bar wider than the phone and iOS let it
       drift sideways under a scrolling thumb (session 30, Single Fin made
       seven). Now a long label shortens instead. */
    min-width: 0;
    flex-direction: column;
    justify-content: center;
    gap: 2px;
    padding: 6px 2px;
    min-height: var(--touch-min);
    border-radius: 0;
    font-size: var(--text-caption);
    text-align: center;
    white-space: nowrap;
  }

  .q-nav__item .q-nav__label {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .q-sidebar { overflow-x: hidden; }

  /* Home is the launcher (2 Oct, his 1A and 2A): on a phone Quiver's own
     bar is never drawn. Inside an app the bar is that app's pages, and the
     ‹ at the top goes home. */
  .q-app .q-sidebar { display: none; }
  .q-topbar__back { display: grid; margin-right: -6px; }

  .q-app.has-appbar .q-appbar {
    display: flex;
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 20;
    min-height: var(--tabbar-h);
    padding-bottom: env(safe-area-inset-bottom);
    background: var(--bg-raised);
    border-top: 1px solid var(--side-separator);
  }

  .q-appbar__item {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    padding: 6px 2px;
    min-height: var(--touch-min);
    color: var(--side-text);
    font-size: var(--text-caption);
    text-decoration: none;
    white-space: nowrap;
  }

  .q-appbar__item.is-active { color: var(--accent); }

  .q-appbar__label {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  /* A filled pill is a sidebar idea. A tab bar says "selected" with the
     tint colour on the icon and label. */
  .q-nav__item.is-active {
    background: none;
    color: var(--accent);
  }

  .q-nav__item:hover { background: none; }

  .q-main { padding-bottom: env(safe-area-inset-bottom); }

  /* Clear the app's fixed bar, including whatever the home indicator takes. */
  .q-app.has-appbar .q-main {
    padding-bottom: calc(var(--tabbar-h) + env(safe-area-inset-bottom));
  }

  /* The header is chrome, and on a phone chrome should get out of the way.
     The spoke's own description is a desktop nicety — the title and the
     icon already say where you are, and three lines of header at the top of
     a 375pt screen is three lines not showing the day. */
  .q-topbar {
    gap: 10px;
    padding: 10px 16px;
    padding-top: calc(10px + env(safe-area-inset-top));
  }

  .q-topbar__title { font-size: var(--text-h3); }
  .q-topbar__desc { display: none; }

  .q-topbar__link {
    min-height: 44px;
    padding: 8px 12px;
  }

  .q-pane {
    margin: 0 12px 12px;
    margin-left: calc(12px + env(safe-area-inset-left));
    margin-right: calc(12px + env(safe-area-inset-right));
  }

  .q-pane__body { padding: 16px; }

  /* ---- a spoke that takes the phone's whole viewport ----
     Blue Lobsters is used on a touchline and its own five tabs are its
     navigation; two bottom tab bars, one above the other, is not a layout.
     On a desktop it is a spoke like any other — sidebar, topbar, pane — which
     is Charlie's call of 11 September 2026, reversing the 30 August one for
     the wide screen only. So this is the *phone* that is the exception now,
     and it is chrome the page gives up rather than markup it replaces. */
  .q-app--takeover .q-sidebar,
  .q-app--takeover .q-topbar { display: none; }

  .q-app--takeover .q-main { padding-bottom: 0; }

  .q-app--takeover .q-pane {
    margin: 0;
    border: none;
    border-radius: 0;
    box-shadow: none;
  }

  .q-app--takeover .q-pane__body { padding: 0; }

  /* A modal with 20px of scrim around it on a phone is a modal you cannot
     read. It takes the screen, the way the coaching spoke's sheets do. */
  .q-sheet { padding: 0; }

  .q-sheet__panel {
    max-width: none;
    max-height: none;
    min-height: 100dvh;
    border-radius: 0;
  }

  /* Home: no pane chrome, just the rows. */
  .q-app--home .q-pane { background: none; border: 0; box-shadow: none; }
  .q-app--home .q-pane__body { padding: 4px 0 16px; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

/* ---- the side button's setup page (2 Oct 2026) ----
   Visited once. The key, when there is one, is the loud thing; the address
   underneath is reference, so it is dashed and quiet. */
.q-capture { display: flex; flex-direction: column; gap: 14px; max-width: 640px; }
.q-capture__h { display: flex; align-items: center; gap: 8px; margin: 0 0 6px; font-size: var(--text-h3); font-weight: 600; }
.q-capture__card { padding: 14px 16px; border-radius: var(--radius-card); background: var(--bg-raised); border: 1px solid var(--separator); display: grid; gap: 8px; justify-items: start; }
.q-capture__card--quiet { border-style: dashed; background: none; }
.q-capture__key { padding: 14px 16px; border-radius: var(--radius-card); border: 2px solid var(--accent); background: var(--bg-raised); display: grid; gap: 8px; }
.q-capture__key p, .q-capture__card p { margin: 0; }
.q-capture__code { display: block; max-width: 100%; overflow-wrap: anywhere; font-family: var(--font-mono); font-size: var(--text-small); padding: 8px 10px; border-radius: var(--radius-tile); background: var(--fill-quaternary); user-select: all; }
.q-capture__quiet { color: var(--label-secondary); font-size: var(--text-small); }
/* Once the phone has a key, making another is the quiet choice: it stops
   the old one working. */
.q-capture__again { background: none; color: var(--accent); border: 1px dashed var(--accent); }

/* ---- something went wrong (2 Oct 2026) ----
   A way out, loud; trying again, quiet. */
.q-oops { display: grid; justify-items: center; gap: 12px; text-align: center; }
.q-oops__icon { color: var(--status-warning); }
.q-oops__title { margin: 0; font-size: var(--text-h2); }
.q-oops__hint { margin: 0; color: var(--label-secondary); }
.q-oops__home { display: inline-flex; align-items: center; gap: 8px; text-decoration: none; }
.q-oops__again {
  border: 0;
  background: none;
  color: var(--accent);
  font: inherit;
  font-size: var(--text-small);
  min-height: var(--touch-min);
  cursor: pointer;
}
