:root {
  --color-bg: #f5f6f8;
  --color-surface: #ffffff;
  --color-text: #1c1e21;
  --color-muted: #6b7280;
  --color-primary: #1f6f50;
  --color-primary-contrast: #ffffff;
  --color-border: #e2e4e8;
  --color-danger: #b3261e;
  --color-warning: #8a5a00;
  --color-warning-bg: #fff6e0;
  --radius: 14px;
  --nav-height: 64px;
  --sidebar-width: 264px;
  --chat-column-width: 720px;
  --workspace-bg: #f7f8fa;
  --color-primary-tint: #e3f4ea;

  /* Schedule Month Calendar (2026-08-08) - restrained, INTERNAL-ONLY
     category accent colours for DISPLAY purposes only - see
     docs/project_context.md, "Category rendering". Outlook's own
     category COLOUR is not retrievable without a `MailboxSettings.Read`
     permission this app deliberately does not request (see
     scheduling.models.CalendarCategory's own docstring and
     scheduling/services/calendar_view.py::category_slug()) - these five
     pairs (accent + a light tint for the chip background) are this app's
     OWN mapping of the five existing managed category NAMES, never a
     substitute for or change to Outlook's actual category business
     logic. "none" is the neutral fallback for an uncategorized event. */
  --cal-exams: #b45309;
  --cal-exams-bg: #fdf1e2;
  --cal-meetings: #1d4ed8;
  --cal-meetings-bg: #e8eefd;
  --cal-personal: #7c3aed;
  --cal-personal-bg: #f1ebfd;
  --cal-school-break: #15803d;
  --cal-school-break-bg: #e5f3ea;
  --cal-staff-leave: #0f766e;
  --cal-staff-leave-bg: #e2f2f1;
  --cal-none: #6b7280;
  --cal-none-bg: #eef0f2;
}

* {
  box-sizing: border-box;
}

html, body {
  margin: 0;
  padding: 0;
  background: var(--color-bg);
  color: var(--color-text);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  font-size: 16px;
  line-height: 1.45;
}

body {
  padding-bottom: calc(var(--nav-height) + 16px + env(safe-area-inset-bottom, 0px));
}

/* Messaging-app header/composer (2026-07-29, Assistant mobile UX -
   hotfix): an earlier version of this made `body.chat-page` a
   fixed-height (`100dvh`) flex column with `overflow: hidden`, relying
   on `.chat-messages` alone to scroll internally. On a real iPhone that
   collapsed the entire page (header, messages, composer all became
   effectively invisible - only the separately `position: fixed`
   `.bottom-nav` remained) - a known, longstanding class of iOS Safari
   bug where `100vh`/`100dvh` + flex + `overflow: hidden` can compute
   available height incorrectly, especially around the dynamic
   toolbar. Fixed by using the SAME proven pattern `.app-header` already
   used successfully everywhere else in this app: plain
   `position: sticky`, on an otherwise perfectly normal scrolling page.
   `body.chat-page` no longer changes height/overflow/display at all -
   the Assistant page scrolls exactly like every other page; only
   `.assistant-header` (sticky to the top) and `.chat-composer` (sticky
   to just above `.bottom-nav`) "stick" as you scroll past them. */

a {
  color: var(--color-primary);
  text-decoration: none;
}

/* Desktop app-shell sidebar (2026-07-30) - see docs/project_context.md,
   "Desktop app shell". Hidden entirely below 900px on purpose - mobile
   keeps its own separate `.bottom-nav` tab bar (further down, completely
   untouched by this rule or the ones inside `@media (min-width: 900px)`
   below) rather than one element trying to serve both roles. */
.sidebar {
  display: none;
}

.app-header {
  position: sticky;
  top: 0;
  z-index: 10;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 16px;
  background: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
}

.app-header h1 {
  font-size: 1.1rem;
  margin: 0;
}

.app-header .logout-form button {
  background: none;
  border: none;
  color: var(--color-muted);
  font-size: 0.95rem;
  padding: 8px;
}

.container {
  max-width: 640px;
  margin: 0 auto;
  padding: 16px;
}

/* `.assistant-header` is `position: fixed` on the Assistant page
   (2026-07-29 keyboard/scroll hotfix) - removed from `.container`'s
   normal flow, so anything that's STILL in that normal flow (on a
   read-only historical day: `.chat-readonly-banner`, since `#chat-messages`
   is also fixed there - see static/js/chat_layout.js) would otherwise
   render underneath the fixed header. This reserves that space; ~64px
   is a close static match for the header's own ~60px rendered height,
   refined by nothing further since the banner doesn't need pixel-exact
   spacing the way the scrollable message list does. */
body.chat-page .container {
  padding-top: 64px;
}

.bottom-nav {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  height: calc(var(--nav-height) + env(safe-area-inset-bottom, 0px));
  padding-bottom: env(safe-area-inset-bottom, 0px);
  background: var(--color-surface);
  border-top: 1px solid var(--color-border);
  display: flex;
  z-index: 10;
}

.bottom-nav a {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  color: var(--color-muted);
  font-size: 0.8rem;
  font-weight: 500;
}

.bottom-nav a.active {
  color: var(--color-primary);
}

.bottom-nav .icon {
  font-size: 1.3rem;
}

/* Perceived-responsiveness feedback (2026-07-29) - subtle, not a loading
   screen: `:active` gives an instant native tap response before any JS
   runs; `.is-navigating` (added by static/js/nav.js on tap, removed by
   the next page's own fresh load) keeps that dimmed feedback visible for
   the remainder of an in-flight full-page navigation and blocks a
   duplicate tap from re-triggering it. Neither a loading spinner nor an
   SPA - these are still plain <a href> page loads. */
.bottom-nav a:active,
.bottom-nav a.is-navigating {
  opacity: 0.6;
}

.card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: 16px;
  margin-bottom: 12px;
}

.card-link {
  display: block;
  color: inherit;
}

.card-title {
  font-weight: 600;
  font-size: 1.05rem;
  margin-bottom: 4px;
}

.card-meta {
  color: var(--color-muted);
  font-size: 0.9rem;
}

.badge {
  display: inline-block;
  padding: 3px 10px;
  border-radius: 999px;
  font-size: 0.78rem;
  font-weight: 600;
}

.badge-ACTIVE { background: #e3f4ea; color: #1f6f50; }
.badge-INACTIVE { background: #eee; color: var(--color-muted); }
.badge-ARCHIVED { background: #f1e6e6; color: var(--color-danger); }

.badge-SCHEDULED { background: var(--color-warning-bg); color: var(--color-warning); }
.badge-CANCELLED { background: #f1e6e6; color: var(--color-danger); }
.badge-COMPLETED { background: #e3f4ea; color: #1f6f50; }

.badge-LOW { background: #eee; color: var(--color-muted); }
.badge-NORMAL { background: #eee; color: var(--color-muted); }
.badge-HIGH { background: var(--color-warning-bg); color: var(--color-warning); }
.badge-URGENT { background: #f1e6e6; color: var(--color-danger); }

/* Knowledge Base document status (2026-07-31) - see knowledge/models.py::DocumentStatus. */
.badge-CURRENT { background: #e3f4ea; color: #1f6f50; }
.badge-PROCESSING { background: var(--color-warning-bg); color: var(--color-warning); }
.badge-SUPERSEDED { background: #eee; color: var(--color-muted); }
.badge-FAILED { background: #f1e6e6; color: var(--color-danger); }

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 48px;
  padding: 0 20px;
  border-radius: var(--radius);
  border: none;
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
  width: 100%;
  margin-bottom: 10px;
}

.btn-primary {
  background: var(--color-primary);
  color: var(--color-primary-contrast);
}

.btn-secondary {
  background: var(--color-surface);
  color: var(--color-text);
  border: 1px solid var(--color-border);
}

.btn-danger {
  background: var(--color-surface);
  color: var(--color-danger);
  border: 1px solid var(--color-danger);
}

.btn-inline {
  width: auto;
  min-height: 40px;
  padding: 0 16px;
}

.search-row {
  display: flex;
  gap: 8px;
  margin-bottom: 12px;
}

input[type=text], input[type=search], input[type=email], input[type=tel], input[type=password], select, textarea {
  width: 100%;
  min-height: 48px;
  padding: 10px 14px;
  font-size: 1rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-surface);
  color: var(--color-text);
}

label {
  display: block;
  font-weight: 600;
  font-size: 0.9rem;
  margin: 14px 0 6px;
}

.filter-row {
  display: flex;
  gap: 8px;
  overflow-x: auto;
  margin-bottom: 14px;
}

.filter-pill {
  flex: none;
  padding: 8px 14px;
  border-radius: 999px;
  border: 1px solid var(--color-border);
  color: var(--color-muted);
  font-size: 0.85rem;
  font-weight: 600;
  white-space: nowrap;
}

.filter-pill.active {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: var(--color-primary-contrast);
}

.detail-row {
  display: flex;
  justify-content: space-between;
  padding: 10px 0;
  border-bottom: 1px solid var(--color-border);
  gap: 12px;
}

.detail-row:last-child {
  border-bottom: none;
}

.detail-label {
  color: var(--color-muted);
  font-size: 0.9rem;
}

.detail-value {
  font-weight: 500;
  text-align: right;
}

.warning-box {
  background: var(--color-warning-bg);
  color: var(--color-warning);
  border-radius: var(--radius);
  padding: 14px;
  margin-bottom: 14px;
  font-size: 0.92rem;
}

.empty-state {
  text-align: center;
  color: var(--color-muted);
  padding: 40px 16px;
}

/* Empty-chat example prompts (Knowledge UX & Polish phase, 2026-08-04) -
   see docs/project_context.md, "Knowledge UI improvements". Categorized
   by capability (Calendar/Tasks/Knowledge/Staff) rather than one generic
   line, kept compact - never more than a handful of short lines per
   category - so a brand-new conversation doesn't feel overcrowded. */
.chat-empty-intro {
  margin: 0 0 16px;
}

.chat-example-groups {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 16px 28px;
  text-align: left;
  max-width: 560px;
  margin: 0 auto;
}

.chat-example-group {
  min-width: 200px;
}

.chat-example-group-title {
  font-size: 0.78rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--color-primary);
  margin-bottom: 6px;
}

.chat-example-item {
  font-size: 0.88rem;
  color: var(--color-text);
  padding: 3px 0;
}

.chat-example-item::before {
  content: "• ";
  color: var(--color-muted);
}

/* Shared Schedule/Tasks page title - mobile size here; the desktop
   `@media (min-width: 900px)` block below restyles `.page-header` into a
   flex row and bumps `.page-header h2` to 1.4rem, matching the
   Assistant workspace header's weight (see docs/project_context.md,
   "Desktop app shell"). */
.page-header h2 {
  font-size: 1.1rem;
  margin: 0;
}

.messages {
  list-style: none;
  padding: 0;
  margin: 0 0 14px;
}

.messages li {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: 12px 14px;
  margin-bottom: 8px;
  font-size: 0.92rem;
}

.form-actions {
  margin-top: 20px;
}

/* The one scrolling region (2026-07-29, keyboard/scroll hotfix -
   supersedes an earlier "ordinary flowing content, whole page scrolls"
   version). `.assistant-header`/`.chat-composer` are now `position:
   fixed`, taking them out of the page's normal flow - `#chat-messages`
   becomes the only thing that scrolls, via its OWN `position: fixed` +
   `overflow-y: auto`, with `top`/`bottom` kept in sync with the header's/
   composer's ACTUAL rendered heights by `static/js/chat_layout.js`
   (never `vh`/`dvh` units or flex-grow sizing - every offset here is a
   real measured pixel value, recalculated on resize/keyboard-open/close
   - see that file's own module docstring for why this avoids the
   documented iOS Safari bug class that broke this page once already).
   The values below are only a same-page fallback for the instant before
   JS finishes its first measurement (and the permanent behaviour if JS
   fails entirely) - not meant to be pixel-accurate on their own. */
.chat-messages {
  position: fixed;
  left: 16px;
  right: 16px;
  max-width: 608px;
  margin: 0 auto;
  top: 64px;
  bottom: 140px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding-top: 4px;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}

.chat-row {
  display: flex;
}

.chat-row-user {
  justify-content: flex-end;
}

.chat-row-assistant {
  justify-content: flex-start;
}

.chat-bubble {
  max-width: 85%;
  height: auto;
  min-height: 0;
  padding: 9px 13px;
  border-radius: var(--radius);
  font-size: 0.95rem;
  white-space: pre-wrap;
  overflow-wrap: break-word;
}

.chat-bubble-user {
  background: var(--color-primary);
  color: var(--color-primary-contrast);
  border-bottom-right-radius: 4px;
}

.chat-bubble-assistant {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-bottom-left-radius: 4px;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
  /* Markdown rendering (Knowledge UX & Polish phase, 2026-08-04) - see
     docs/project_context.md, "Markdown rendering architecture". Assistant
     replies now render real block-level HTML (<p>/<ul>/<table>/etc. - see
     assistant/services/rendering.py::render_markdown()), so the shared
     `.chat-bubble` rule's `white-space: pre-wrap` (still correct for the
     plain-text+<br> `.chat-bubble-user` path) would otherwise preserve
     stray whitespace BETWEEN those block tags as visible extra blank
     lines - overridden back to normal here; each element's own
     margin/padding below controls real spacing instead. */
  white-space: normal;
}

/* Markdown element styling, scoped to assistant replies only - kept
   compact/subordinate to the surrounding chat UI rather than looking like
   a full document (a chat bubble is narrow) - see docs/project_context.md,
   "Markdown rendering architecture". */
.chat-bubble-assistant p {
  margin: 0 0 8px;
}

.chat-bubble-assistant p:last-child {
  margin-bottom: 0;
}

.chat-bubble-assistant h1,
.chat-bubble-assistant h2,
.chat-bubble-assistant h3,
.chat-bubble-assistant h4,
.chat-bubble-assistant h5,
.chat-bubble-assistant h6 {
  margin: 12px 0 6px;
  line-height: 1.25;
}

.chat-bubble-assistant h1:first-child,
.chat-bubble-assistant h2:first-child,
.chat-bubble-assistant h3:first-child {
  margin-top: 0;
}

.chat-bubble-assistant h1 { font-size: 1.15rem; }
.chat-bubble-assistant h2 { font-size: 1.08rem; }
.chat-bubble-assistant h3,
.chat-bubble-assistant h4,
.chat-bubble-assistant h5,
.chat-bubble-assistant h6 { font-size: 1rem; }

.chat-bubble-assistant ul,
.chat-bubble-assistant ol {
  margin: 0 0 8px;
  padding-left: 1.4em;
}

.chat-bubble-assistant li {
  margin-bottom: 2px;
}

.chat-bubble-assistant blockquote {
  margin: 0 0 8px;
  padding: 2px 10px;
  border-left: 3px solid var(--color-border);
  color: var(--color-muted);
}

.chat-bubble-assistant code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.88em;
  background: rgba(0, 0, 0, 0.06);
  border-radius: 4px;
  padding: 0.1em 0.35em;
}

.chat-bubble-assistant pre {
  margin: 0 0 8px;
  padding: 8px 10px;
  background: rgba(0, 0, 0, 0.06);
  border-radius: 6px;
  overflow-x: auto;
}

.chat-bubble-assistant pre code {
  background: none;
  padding: 0;
}

/* A Markdown table can easily be wider than a narrow chat bubble - scroll
   the table itself horizontally rather than forcing the whole bubble
   (and the page) wider. */
.chat-bubble-assistant table {
  display: block;
  overflow-x: auto;
  margin: 0 0 8px;
  border-collapse: collapse;
  font-size: 0.92em;
}

.chat-bubble-assistant th,
.chat-bubble-assistant td {
  border: 1px solid var(--color-border);
  padding: 4px 8px;
  text-align: left;
}

.chat-bubble-assistant hr {
  border: none;
  border-top: 1px solid var(--color-border);
  margin: 10px 0;
}

/* Assistant bubble footer (2026-07-29): timestamp + the existing speaker
   replay button together, bottom-right, subtle - never enlarged, never
   competing visually with the message content above it. */
.chat-bubble-footer {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 2px;
  margin-top: 2px;
}

.chat-bubble-timestamp {
  font-size: 0.7rem;
  color: var(--color-muted);
  opacity: 0.8;
}

/* Chat UX (Phase 5 refactor): a message bubble appended by JS fades/slides
   in instead of just popping into place. Understated on purpose - short
   duration, small distance. Respects prefers-reduced-motion below. */
.chat-bubble-enter {
  animation: chat-bubble-in 0.18s ease-out;
}

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

/* Thinking indicator: three subtle bouncing dots, shown while a message is
   being processed and removed once the response arrives. */
.chat-thinking {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 12px 14px;
}

.chat-thinking .dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  opacity: 0.4;
  animation: chat-thinking-bounce 1.1s ease-in-out infinite;
}

.chat-thinking .dot:nth-child(2) {
  animation-delay: 0.15s;
}

.chat-thinking .dot:nth-child(3) {
  animation-delay: 0.3s;
}

@keyframes chat-thinking-bounce {
  0%, 80%, 100% {
    opacity: 0.3;
    transform: translateY(0);
  }
  40% {
    opacity: 0.9;
    transform: translateY(-3px);
  }
}

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

  .chat-thinking .dot {
    animation: none;
    opacity: 0.6;
  }
}

.chat-bubble a {
  color: inherit;
  text-decoration: underline;
}

/* Visually-hidden but screen-reader-accessible - used for the composer's
   <label> now that the send row is icon-driven, not text-driven. */
.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;
}

/* Assistant header row: history/back icon + conversation date + mute/
   unmute "Voice replies" icon button (mobile messaging-app redesign,
   2026-07-29) - this IS the page's one header on the Assistant page
   (the generic site header is suppressed there, see
   templates/assistant/detail.html's `{% block header %}`); the
   conversation's own date replaces a redundant "Nozomi"
   title. ~60px tall including padding, matching the requested 56-64px
   compact header target.

   `position: sticky` (hotfix, 2026-07-29) - the exact same proven
   technique `.app-header` already uses on every other page, rather than
   the fixed-height flex-shell approach that broke on real iOS Safari
   (see the note near `body { padding-bottom }` above). Needs its own
   opaque `background` (already set below) so scrolled-past message
   content doesn't show through underneath it while stuck. */
/* position: fixed (2026-07-29, keyboard/scroll hotfix): `position:
   sticky` let the whole page scroll together, which meant the header/
   composer could appear to scroll away along with the message list -
   most noticeably when tapping the composer's textarea, where iOS
   Safari's own "scroll focused input into view" behaviour and the
   on-screen keyboard interact poorly with sticky elements. Switching to
   `position: fixed` (not `height: 100vh` + `display: flex` +
   `overflow: hidden` on `body` - that combination is the documented,
   real iOS Safari bug that broke this page once already; see the note
   above and docs/CLAUDE.md) removes the header from the page's normal
   flow entirely, so it never moves regardless of how `#chat-messages`
   scrolls. `left`/`right`/`max-width`/`margin` replicate `.container`'s
   own centered-max-640px layout, since a fixed element no longer
   inherits that from being `.container`'s in-flow child. See
   `static/js/chat_layout.js` for how `#chat-messages`'s own top/bottom
   offsets are kept in sync with this and `.chat-composer`'s actual
   rendered heights - this CSS alone only positions the header itself. */
.assistant-header {
  position: fixed;
  top: 0;
  left: 16px;
  right: 16px;
  max-width: 608px;
  margin: 0 auto;
  z-index: 30;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  min-height: 60px;
  padding: 10px 4px;
  background: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
}

/* Mobile-safe wrappers around the header's own left (icon-btn + title)
   and right (date card + voice toggle) groups (2026-07-30 desktop
   redesign) - a bare `<div>` defaults to `display: block`, which would
   stack the icon-btn/title vertically instead of inline, so this must be
   declared unconditionally (not just inside the desktop media query) to
   keep the existing mobile appearance exactly as it was. */
.assistant-header-left,
.assistant-header-right {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
}

.assistant-title {
  font-size: 1.1rem;
  margin: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Desktop-only greeting/date-card (2026-07-30) - hidden on mobile, where
   `.assistant-title-mobile` (the existing compact history-icon + date
   title) is shown instead. Swapped the other way around inside
   `@media (min-width: 900px)` below. */
.assistant-header-greeting,
.date-card,
.composer-helper-text {
  display: none;
}

.icon-btn {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-muted);
  cursor: pointer;
  padding: 0;
}

/* Global spoken-replies control: a compact pill (icon + short state
   label) rather than a plain icon-only circle, so ON/OFF is readable at
   a glance without relying on colour/tooltip/aria-label alone (the icon
   geometry itself still differs too - see the two <svg>s above). Widens
   `.icon-btn`'s circle into a pill just enough for one short word. */
.voice-replies-btn {
  width: auto;
  height: 32px;
  border-radius: 999px;
  gap: 5px;
  padding: 0 10px 0 8px;
}

.voice-replies-label {
  font-size: 0.75rem;
  font-weight: 600;
  white-space: nowrap;
}

.icon-btn[aria-pressed="true"] {
  color: var(--color-primary);
  border-color: var(--color-primary);
}

/* Message composer: a native-messaging-app row - circular mic, rounded
   input, circular Send, all in one line (2026-07-29 mobile redesign;
   previously the mic was a separate, full-width control below this
   row) - see docs/project_context.md, "Composer redesign".

   `position: fixed` (2026-07-29, keyboard/scroll hotfix - supersedes an
   earlier `position: sticky` version): sticky let the whole page scroll
   as one document, which meant the composer could appear to scroll away
   with the message list - most noticeable when tapping the textarea,
   where iOS Safari's own "scroll focused input into view" behaviour and
   the keyboard interact poorly with sticky elements. `position: fixed`
   (never `height: 100vh` + `display: flex` + `overflow: hidden` on
   `body` - that combination is the documented, real iOS Safari bug that
   broke this page once already; see the note near
   `body { padding-bottom }` above and docs/CLAUDE.md) removes it from
   the page's normal flow entirely. `bottom` still starts at the same
   `calc(var(--nav-height) + env(safe-area-inset-bottom, 0px))` formula
   shared with `.bottom-nav`'s own height - `static/js/chat_layout.js`
   overrides this bottom value (as an inline style) only while the
   on-screen keyboard is open, sliding the composer up to sit flush
   above it and hiding `.bottom-nav` for that duration; it restores the
   CSS-computed value (by clearing the inline style) once the keyboard
   closes. `left`/`right`/`max-width`/`margin` replicate `.container`'s
   own centered-max-640px layout, since a fixed element no longer
   inherits that from being `.container`'s in-flow child. Needs its own
   opaque `background` (already set below) so scrolled message content
   doesn't show through underneath it. */
.chat-composer {
  position: fixed;
  left: 16px;
  right: 16px;
  max-width: 608px;
  margin: 0 auto;
  bottom: calc(var(--nav-height) + env(safe-area-inset-bottom, 0px));
  z-index: 20;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: 10px;
  box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
}

/* Composer alignment hotfix (2026-07-29): was `align-items: flex-end`,
   which bottom-aligned the fixed-size mic/send circles against the
   textarea's own rendered height - fine only when that height happened
   to equal the circles' own height. The form's `rows="2"` (see
   assistant/forms.py) plus this padding made the textarea render
   noticeably taller than 44px, so the circles visibly sat low/off-centre
   against it (the real-device symptom). `align-items: center` plus a
   single shared ~48px sizing for mic/textarea/send (below) keeps all
   three vertically centred regardless of the textarea's exact height. */
.composer-row {
  display: flex;
  align-items: center;
  gap: 10px;
}

.composer-row textarea {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 48px;
  /* Auto-grow ceiling (2026-08-01, Calendar Capabilities phase) - see
     static/js/assistant_chat.js::resizeComposerTextarea(), which reads
     this computed value rather than hard-coding it, so this one CSS rule
     stays the single source of truth for both the mobile and (overridden
     below) desktop maximum. */
  max-height: 120px;
  margin: 0;
  border-radius: 24px;
  padding: 13px 16px;
}

.btn-send {
  flex: 0 0 auto;
  width: 48px;
  height: 48px;
  border-radius: 50%;
  border: none;
  background: var(--color-primary);
  color: var(--color-primary-contrast);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: 0;
}

/* Stop-generating state (Chat cancellation phase, 2026-08-05) - see
   docs/project_context.md, "Stop generating (chat cancellation)". The
   SAME button/element/size/border-radius as .btn-send above - only the
   background changes, and static/js/assistant_chat.js swaps which of
   the two child <svg> icons is visible (never a second button, never a
   layout shift). A muted neutral tone, not --color-danger - stopping a
   reply is a deliberate, ordinary action, not an error. */
.btn-send.btn-send-stop {
  background: var(--color-muted);
}

/* Voice input (Phase 7A; restyled compact/circular 2026-07-29) - a
   plain circular icon button inline with the text field, matching Send -
   progressive enhancement only, static/js/assistant_chat.js unhides
   `.mic-btn` when MediaRecorder/getUserMedia are actually supported (a
   secure context is required for either to exist at all). The visible
   "Tap to talk"/"Listening…"/"Transcribing…" text moved from a label
   inside this button (which no longer has room for text at this size)
   to the existing `#voice-status` bar below the row - the button's
   `aria-label` still carries the same text for assistive tech, and the
   underlying state machine/transitions are entirely unchanged. */
.mic-btn {
  flex: 0 0 auto;
  width: 48px;
  height: 48px;
  border-radius: 50%;
  border: 1px solid var(--color-border);
  background: var(--color-bg);
  color: var(--color-muted);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: 0;
}

.mic-btn-icon {
  display: flex;
  align-items: center;
  justify-content: center;
}

.mic-waveform {
  display: none;
  align-items: center;
  gap: 2px;
}

.mic-btn.recording {
  border-color: var(--color-danger);
  background: var(--color-danger);
  color: #fff;
  animation: mic-pulse 1.2s ease-in-out infinite;
}

.mic-btn.recording .mic-icon {
  display: none;
}

.mic-btn.recording .mic-waveform {
  display: flex;
}

.mic-btn.transcribing {
  opacity: 0.6;
}

.voice-status-text {
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--color-muted);
}

.mic-waveform-bar {
  width: 3px;
  height: 10px;
  border-radius: 2px;
  background: currentColor;
  animation: mic-waveform-bounce 0.9s ease-in-out infinite;
}

.mic-waveform-bar:nth-child(2) {
  animation-delay: 0.15s;
}

.mic-waveform-bar:nth-child(3) {
  animation-delay: 0.3s;
}

@keyframes mic-waveform-bounce {
  0%, 100% {
    transform: scaleY(0.5);
  }
  50% {
    transform: scaleY(1.4);
  }
}

@keyframes mic-pulse {
  0%, 100% {
    box-shadow: 0 0 0 0 rgba(211, 47, 47, 0.4);
  }
  50% {
    box-shadow: 0 0 0 8px rgba(211, 47, 47, 0);
  }
}

.voice-status {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: 8px;
  font-size: 0.85rem;
}

.voice-error {
  color: var(--color-danger);
}

.voice-cancel-btn {
  min-height: 32px;
  padding: 2px 12px;
  font-size: 0.8rem;
  width: auto;
  margin: 0;
}

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

  .mic-waveform-bar {
    animation: none;
  }
}

/* Bubble-whitespace re-fix (2026-07-29): this was `min-width/min-height:
   36px` - a generic touch-target size that's fine for a standalone
   control, but here it sits inline with a 16px icon inside the compact
   `.chat-bubble-footer` row. Since the footer has no explicit height, its
   rendered height is set by its TALLEST child - the 36px button, not the
   ~16px timestamp text - which left roughly 10px of invisible padding
   above AND below the actual visible glyph. Stacked on `.chat-bubble`'s
   own 9px bottom padding, that read as a large blank gap below the
   timestamp/speaker row on a real device, even though `.chat-bubble`/
   `.chat-bubble-footer` themselves have no height/flex/absolute-position
   anti-pattern (confirmed - see AssistantBubbleWhitespaceCssTests). 24px
   keeps the icon comfortably tappable as a secondary, optional control
   (not the primary composer actions, which keep their own 48px target)
   while letting the footer row's real height track the visible content
   instead of an oversized invisible box. */
/* Second bubble-whitespace pass (2026-07-29): the 36px->24px min-height
   fix alone didn't fully close the gap on the real device - unlike
   `.mic-btn`/`.btn-send` (which already reset `padding: 0`), this rule
   never zeroed the browser's own default <button> padding, and never
   neutralized native appearance. iOS Safari in particular can keep
   native form-control padding/min-size on a <button> even after
   `border`/`background` are overridden, unless `appearance: none` is
   set too - the combination of that leftover UA padding plus the native
   appearance is what was still adding height below the visible icon. */
.speaker-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 24px;
  min-height: 24px;
  padding: 0;
  margin: 0;
  border: none;
  background: transparent;
  appearance: none;
  -webkit-appearance: none;
  cursor: pointer;
  color: inherit;
  opacity: 0.7;
}

.speaker-btn-icon {
  display: block;
}

.speaker-btn:hover {
  opacity: 1;
}

.speaker-btn-loading {
  opacity: 0.4;
  cursor: wait;
}

.speaker-btn-playing {
  opacity: 1;
  color: var(--color-primary);
  animation: mic-pulse 1.2s ease-in-out infinite;
}

.speaker-btn-error {
  color: var(--color-danger);
  opacity: 1;
}

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

@media (min-width: 720px) and (max-width: 899px) {
  .bottom-nav {
    max-width: 640px;
    left: 50%;
    transform: translateX(-50%);
    border-radius: var(--radius) var(--radius) 0 0;
    border: 1px solid var(--color-border);
    border-bottom: none;
  }
}

/* ==========================================================================
   Desktop app shell (2026-07-30 redesign) - see docs/project_context.md,
   "Desktop app shell". Replaces the previous >=900px patch (which just
   repositioned the mobile `position: fixed` + JS-measured-offset
   architecture behind a sidebar) with a proper CSS Grid application
   shell: `body` becomes a 2-column grid (sidebar | workspace); the
   Assistant page's header/messages/composer become ordinary flex items
   inside `.container` (no `position: fixed`, no pixel offsets, no JS
   involvement at all - `static/js/chat_layout.js` explicitly no-ops at
   this width, see its own module docstring) with `min-height: 0` on the
   scrolling message list, the standard, well-supported way to make one
   nested region scroll inside a fixed-height flex/grid column. This is
   safe specifically because it's scoped to >=900px: the documented iOS
   Safari `100vh`+flex+`overflow:hidden` bug this app was previously
   burned by (see the notes near the top of this file and docs/CLAUDE.md)
   is a dynamic-toolbar quirk of narrow mobile Safari, not desktop
   browsers - mobile (<900px) keeps the exact previous fixed-position/JS
   architecture completely untouched. */
@media (min-width: 900px) {
  body {
    display: grid;
    grid-template-columns: var(--sidebar-width) 1fr;
    padding: 0;
    background: var(--workspace-bg);
  }

  /* The mobile tab bar is fully replaced by `.sidebar` at this width -
     see the note on `.sidebar` near the top of this file. */
  .bottom-nav {
    display: none;
  }

  /* ---- Sidebar ---------------------------------------------------- */

  .sidebar {
    display: flex;
    flex-direction: column;
    position: sticky;
    top: 0;
    height: 100vh;
    padding: 20px 16px;
    background: var(--color-surface);
    border-right: 1px solid var(--color-border);
  }

  .sidebar-brand {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 4px 8px 20px;
  }

  .brand-mark {
    flex: 0 0 auto;
    width: 32px;
    height: 32px;
    border-radius: 9px;
    background: var(--color-primary);
    display: flex;
    align-items: center;
    justify-content: center;
  }

  .sidebar-brand-name {
    font-size: 1rem;
    font-weight: 700;
    line-height: 1.2;
  }

  .sidebar-brand-tagline {
    font-size: 0.78rem;
    color: var(--color-muted);
  }

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

  .sidebar-nav-link {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 9px 12px;
    border-radius: var(--radius);
    color: var(--color-text);
    font-size: 0.92rem;
    font-weight: 500;
  }

  .sidebar-nav-link .sidebar-nav-icon {
    color: var(--color-muted);
  }

  .sidebar-nav-link:hover {
    background: var(--workspace-bg);
  }

  .sidebar-nav-link.active {
    background: var(--color-primary-tint);
    color: var(--color-primary);
    font-weight: 600;
  }

  .sidebar-nav-link.active .sidebar-nav-icon {
    color: var(--color-primary);
  }

  .sidebar-section {
    margin-top: 20px;
    padding-top: 16px;
    border-top: 1px solid var(--color-border);
  }

  .sidebar-section-label {
    padding: 0 12px 6px;
    font-size: 0.72rem;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--color-muted);
  }

  .sidebar-quick-action {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 7px 12px;
    border-radius: var(--radius);
    color: var(--color-muted);
    font-size: 0.86rem;
    font-weight: 500;
  }

  .sidebar-quick-action:hover {
    background: var(--workspace-bg);
    color: var(--color-text);
  }

  .sidebar-spacer {
    flex: 1 1 auto;
  }

  .sidebar-bottom {
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding-top: 14px;
    border-top: 1px solid var(--color-border);
  }

  .sidebar-outlook-status {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 6px 12px;
    color: var(--color-muted);
  }

  .sidebar-bottom-text {
    line-height: 1.3;
  }

  .sidebar-bottom-label {
    font-size: 0.72rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.03em;
  }

  .sidebar-bottom-value {
    display: flex;
    align-items: center;
    gap: 5px;
    font-size: 0.82rem;
  }

  .status-dot {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--color-muted);
    flex: 0 0 auto;
  }

  .status-dot-on {
    background: var(--color-primary);
  }

  .sidebar-logout-form {
    margin: 0;
  }

  .sidebar-logout-btn {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding: 7px 12px;
    background: none;
    border: none;
    border-radius: var(--radius);
    color: var(--color-muted);
    font-size: 0.86rem;
    font-weight: 500;
    text-align: left;
    cursor: pointer;
  }

  .sidebar-logout-btn:hover {
    background: var(--workspace-bg);
    color: var(--color-text);
  }

  /* ---- Workspace (everything right of the sidebar) ---------------- */

  .workspace {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
  }

  .container {
    flex: 1 1 auto;
    width: 100%;
    max-width: 1000px;
    margin: 0 auto;
    padding: 32px 40px;
  }

  /* ---- Assistant page: its own fixed-viewport-height flex column --- */

  body.chat-page .workspace {
    height: 100vh;
  }

  body.chat-page .container {
    max-width: none;
    height: 100vh;
    padding: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;
  }

  .assistant-header {
    position: static;
    flex: 0 0 auto;
    left: auto;
    right: auto;
    top: auto;
    max-width: none;
    margin: 0;
    min-height: 0;
    padding: 24px 40px 16px;
    background: var(--color-surface);
    border-bottom: 1px solid var(--color-border);
    z-index: auto;
  }

  .assistant-title-mobile {
    display: none;
  }

  .assistant-header-greeting {
    display: block;
  }

  .workspace-greeting {
    margin: 0;
    font-size: 1.4rem;
    font-weight: 700;
  }

  .workspace-subtitle {
    margin: 2px 0 0;
    color: var(--color-muted);
    font-size: 0.92rem;
  }

  .date-card {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    line-height: 1.3;
    padding-right: 2px;
  }

  .date-card-date {
    font-size: 0.88rem;
    font-weight: 600;
  }

  .date-card-day {
    font-size: 0.78rem;
    color: var(--color-muted);
  }

  /* The scrolling message list - `flex: 1 1 auto` + `min-height: 0` is
     what lets it be the ONE thing that scrolls inside a fixed-height
     flex column (the header/composer siblings are `flex: 0 0 auto` and
     never shrink) - the standard nested-scroll-container pattern, no
     JS measurement needed on this breakpoint. */
  #chat-messages {
    position: static;
    flex: 1 1 auto;
    min-height: 0;
    left: auto;
    right: auto;
    top: auto;
    bottom: auto;
    max-width: none;
    margin: 0;
    padding: 20px 40px;
    overflow-y: auto;
  }

  /* `#chat-messages` itself spans the full workspace width (a
     comfortable, wide scroll area) - each individual message row is
     centered within a bounded reading column, so bubbles never stretch
     across the whole monitor (Section 6/10). */
  .chat-row {
    max-width: var(--chat-column-width);
    width: 100%;
    margin: 0 auto;
  }

  .chat-bubble {
    max-width: 640px;
    padding: 11px 15px;
    font-size: 0.96rem;
  }

  /* Pale green tint (not the solid mobile green) + dark text - a desktop
     "productivity app" tone, per Section 6. Mobile's own
     `.chat-bubble-user` rule (solid `--color-primary` background, white
     text) is untouched above this media query. */
  .chat-bubble-user {
    background: var(--color-primary-tint);
    color: var(--color-text);
    border: 1px solid #cdeadb;
  }

  .chat-bubble-assistant {
    box-shadow: 0 1px 3px rgba(16, 24, 40, 0.06);
  }

  .chat-composer {
    position: static;
    flex: 0 0 auto;
    left: auto;
    right: auto;
    bottom: auto;
    max-width: var(--chat-column-width);
    width: 100%;
    margin: 0 auto 24px;
    padding: 10px 14px;
    box-shadow: 0 1px 3px rgba(16, 24, 40, 0.06);
  }

  .composer-helper-text {
    display: block;
    margin-top: 6px;
    padding: 0 4px;
    font-size: 0.76rem;
    color: var(--color-muted);
  }

  /* Compact, not oversized, on desktop - keyboard input is the primary
     interaction, so the primary composer controls no longer need to be
     large touch targets. */
  .mic-btn,
  .btn-send {
    width: 40px;
    height: 40px;
  }

  .composer-row textarea {
    min-height: 40px;
    /* Auto-grow (2026-08-01, Calendar Capabilities phase) - see
       static/js/assistant_chat.js::resizeComposerTextarea(), which reads
       this computed max-height rather than hard-coding it. Desktop gets a
       taller ceiling than mobile's 120px (above) since there's more
       vertical chat real estate and no on-screen keyboard fighting for
       space - still well short of "consuming the whole chat". */
    max-height: 200px;
  }

  /* ---- Shared page-header styling (Schedule/Tasks) ----------------- */

  .page-header {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    margin-bottom: 20px;
  }

  .page-header h2 {
    font-size: 1.4rem;
    margin: 0;
  }

  /* Lightweight multi-column layout for Schedule/Tasks page sections
     (Today / Tomorrow / Overdue / Upcoming / Completed) - plain CSS
     grid, no calendar/kanban library. Sections that don't fit this
     wrapper (forms, detail pages) are unaffected. */
  .page-columns {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: 8px 28px;
    align-items: start;
  }

  .page-columns > .page-column h3 {
    margin-top: 0;
  }

  .card {
    box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04);
  }
}

@media (min-width: 1440px) {
  .container {
    max-width: 1100px;
  }

  :root {
    --chat-column-width: 800px;
  }

  .chat-bubble {
    max-width: 680px;
  }
}

/* Daily Briefing drawer (Work Assistant refactor, 2026-08-02) - see
   docs/project_context.md, "Daily Briefing drawer". Mobile-first base
   styles below are a dismissible BOTTOM SHEET; the `@media (min-width:
   900px)` block further down restyles the same markup into a collapsible
   RIGHT-SIDE panel, per the existing desktop-vs-mobile split convention
   used throughout this file. Open/closed state is a plain `.is-open`
   class toggled by static/js/daily_briefing.js - never a server round
   trip, never a stored preference (localStorage only). */

.briefing-toggle {
  position: relative;
}

.briefing-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(16, 24, 40, 0.45);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease;
  z-index: 70;
}

.briefing-backdrop.is-open {
  opacity: 1;
  pointer-events: auto;
}

.briefing-drawer {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  max-height: 80vh;
  background: var(--color-surface);
  border-radius: var(--radius) var(--radius) 0 0;
  box-shadow: 0 -4px 16px rgba(16, 24, 40, 0.12);
  transform: translateY(100%);
  transition: transform 0.25s ease;
  z-index: 71;
  display: flex;
  flex-direction: column;
  padding-bottom: env(safe-area-inset-bottom, 0px);
}

.briefing-drawer.is-open {
  transform: translateY(0);
}

.briefing-drawer-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 16px;
  border-bottom: 1px solid var(--color-border);
  flex-shrink: 0;
}

.briefing-drawer-header h2 {
  margin: 0;
  font-size: 16px;
}

.briefing-drawer-body {
  overflow-y: auto;
  padding: 12px 16px 20px;
}

.briefing-section {
  margin-bottom: 20px;
}

.briefing-section h3 {
  margin: 0 0 8px;
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-muted);
}

.briefing-item {
  padding: 8px 0;
  border-bottom: 1px solid var(--color-border);
  font-size: 14px;
}

.briefing-item:last-child {
  border-bottom: none;
}

.briefing-item-title {
  font-weight: 600;
  display: block;
}

.briefing-item-meta {
  color: var(--color-muted);
  font-size: 13px;
}

.briefing-empty {
  color: var(--color-muted);
  font-size: 14px;
  padding: 4px 0;
}

.briefing-view-all {
  display: inline-block;
  margin-top: 4px;
  font-size: 13px;
  color: var(--color-primary);
}

.briefing-edge-tab {
  display: none;
}

@media (min-width: 900px) {
  .briefing-backdrop {
    display: none;
  }

  .briefing-drawer {
    left: auto;
    top: 0;
    right: 0;
    bottom: 0;
    max-height: none;
    width: 320px;
    border-radius: 0;
    border-left: 1px solid var(--color-border);
    box-shadow: -2px 0 12px rgba(16, 24, 40, 0.08);
    transform: translateX(100%);
    transition: transform 0.2s ease;
    padding-bottom: 0;
  }

  .briefing-drawer.is-open {
    transform: translateX(0);
  }

  /* The collapsed "‹‹" handle is docked to the viewport's own right edge
     (independent of the drawer's transform, so it stays visible while
     the panel itself is off-screen) and slides left by the drawer's own
     width once open, so it always sits at the panel's outer edge. */
  .briefing-edge-tab {
    display: flex;
    align-items: center;
    gap: 4px;
    position: fixed;
    top: 50%;
    right: 0;
    transform: translateY(-50%);
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-right: none;
    border-radius: var(--radius) 0 0 var(--radius);
    padding: 10px 8px;
    cursor: pointer;
    box-shadow: -1px 0 6px rgba(16, 24, 40, 0.06);
    transition: right 0.2s ease;
    z-index: 72;
    color: var(--color-text);
    font-size: 12px;
  }

  .briefing-edge-tab.is-open {
    right: 320px;
  }
}

/* Schedule Month Calendar (2026-08-08) - see docs/project_context.md,
   "Schedule Month Calendar". Mobile-first base rules below: a compact
   7-column grid with date numbers and small colour-dot event indicators
   only (no titles - see docs/project_context.md, "Mobile behaviour");
   tapping a day reveals its full agenda in `.cal-day-panel`, which sits
   in normal document flow directly below the grid. The
   `@media (min-width: 900px)` block further down is what turns event
   chips into full title+time rows and moves the day panel into a side
   column - see that block's own comment. */

.cal-header {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  /* Title and nav are direct siblings with only a small gap between them
     so they visually read as ONE grouped unit ("August 2026   Today  ‹
     ›") - see docs/project_context.md, "Month navigation". Outlook
     status is pushed to the far right via its own `margin-left: auto`
     below instead of an even three-way spread, which used to leave the
     nav controls looking like a separate, disconnected group from the
     month title on a wide desktop viewport. */
  justify-content: flex-start;
  gap: 8px 18px;
  margin-bottom: 12px;
}

.cal-header-title h2 {
  margin: 0;
  font-size: 1.2rem;
  font-weight: 700;
}

.cal-nav {
  display: flex;
  align-items: center;
  gap: 8px;
  order: 3;
  width: 100%;
}

.cal-nav-arrows {
  /* Prev/next are grouped tightly together as a single, visually paired
     control (unlike "Today", which is its own separate action) so the
     "left = previous, right = next" relationship reads unambiguously -
     see docs/project_context.md, "Month navigation". */
  display: flex;
  gap: 2px;
}

.cal-nav-btn {
  min-height: 36px;
  padding: 0 12px;
  margin-bottom: 0;
  width: auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.cal-nav-arrow {
  padding: 0 10px;
}

.cal-header-outlook {
  font-size: 0.8rem;
  margin-left: auto;
}

.cal-outlook-status {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--color-muted);
}

.cal-connect-banner {
  margin-bottom: 14px;
}

.cal-connect-banner .btn {
  margin-top: 10px;
  width: auto;
}

/* ---- Category legend (see docs/project_context.md, "Category legend")
   - deliberately small and unobtrusive, never a dominant page element. ---- */

.cal-legend {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 8px 16px;
  padding: 0;
  margin: 0 0 12px;
  font-size: 0.76rem;
  color: var(--color-muted);
}

.cal-legend-item {
  display: inline-flex;
  align-items: center;
  gap: 5px;
}

.cal-legend-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  display: inline-block;
  flex: 0 0 auto;
}

.cal-legend-exams .cal-legend-dot { background: var(--cal-exams); }
.cal-legend-meetings .cal-legend-dot { background: var(--cal-meetings); }
.cal-legend-personal .cal-legend-dot { background: var(--cal-personal); }
.cal-legend-school-break .cal-legend-dot { background: var(--cal-school-break); }
.cal-legend-staff-leave .cal-legend-dot { background: var(--cal-staff-leave); }

/* ---- Layout: calendar + day panel ---- */

.cal-layout {
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.cal-container {
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--color-surface);
}

.cal-weekday-row {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  background: var(--workspace-bg);
  border-bottom: 1px solid var(--color-border);
}

.cal-weekday {
  padding: 6px 4px;
  text-align: center;
  font-size: 0.68rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  color: var(--color-muted);
}

.cal-grid {
  display: flex;
  flex-direction: column;
}

.cal-week {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  border-bottom: 1px solid var(--color-border);
}

.cal-week:last-child {
  border-bottom: none;
}

.cal-day-cell {
  position: relative;
  min-height: 60px;
  /* A CSS Grid item's automatic minimum width defaults to its CONTENT's
     min-content size unless overridden - without this, one day cell with
     a long event title (or a long attendee/subject string embedded in
     the day's json_script data) could force its own 1fr grid column
     wider than its six siblings in the same week row, breaking the
     "weekday headers and day columns line up exactly" requirement. This
     is the real structural fix for that misalignment - never patch it
     with a per-cell pixel width instead. */
  min-width: 0;
  padding: 4px;
  border-right: 1px solid var(--color-border);
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.cal-day-cell:last-child {
  border-right: none;
}

.cal-day-muted {
  background: var(--workspace-bg);
}

.cal-day-muted .cal-day-number {
  color: var(--color-muted);
}

.cal-day-number {
  align-self: flex-start;
  border: none;
  background: none;
  padding: 2px 7px;
  border-radius: 999px;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--color-text);
  cursor: pointer;
}

.cal-day-number:hover {
  background: var(--workspace-bg);
}

/* Today (see docs/project_context.md, "Current-day schedule") - today is
   marked with `aria-current="date"` in the markup (semantic, not only
   visual) plus this restrained highlight - never an oversized/loud
   treatment. */
.cal-day-today .cal-day-number {
  background: var(--color-primary);
  color: var(--color-primary-contrast);
}

.cal-day-today .cal-day-number:hover {
  background: var(--color-primary);
}

.cal-day-events {
  display: flex;
  flex-wrap: wrap;
  gap: 3px;
  align-content: flex-start;
}

/* Mobile: an event is a small colour-only dot (still a real, focusable,
   clickable <button> - never purely decorative) rather than a title -
   see docs/project_context.md, "Mobile behaviour". Category is still
   never colour-only for assistive tech: each chip carries a real
   `aria-label`/`title` naming the event (see
   templates/scheduling/_calendar_grid.html) and the legend spells out
   every category name in text. */
.cal-event {
  display: inline-flex;
  width: 9px;
  height: 9px;
  padding: 0;
  margin: 0;
  border: none;
  border-radius: 50%;
  cursor: pointer;
  background: var(--cal-none);
}

.cal-event-time,
.cal-event-title {
  display: none;
}

.cal-event-exams { background: var(--cal-exams); }
.cal-event-meetings { background: var(--cal-meetings); }
.cal-event-personal { background: var(--cal-personal); }
.cal-event-school-break { background: var(--cal-school-break); }
.cal-event-staff-leave { background: var(--cal-staff-leave); }
.cal-event-none { background: var(--cal-none); }

.cal-event-more {
  font-size: 0.62rem;
  font-weight: 600;
  color: var(--color-muted);
  background: none;
  border: none;
  padding: 0 2px;
  cursor: pointer;
}

/* ---- Day-detail panel/agenda (see docs/project_context.md, "Clicking a
   day") - a normal in-flow block on mobile, directly below the grid;
   restyled into a sticky side column at >=900px (see that media query). ---- */

.cal-day-panel {
  display: none;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-surface);
  padding: 14px;
}

.cal-day-panel.is-open {
  display: block;
}

.cal-day-panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
}

.cal-day-panel-header h3 {
  margin: 0;
  font-size: 1rem;
}

.cal-day-panel-body {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.cal-day-panel-empty {
  color: var(--color-muted);
  font-size: 0.88rem;
  margin: 0;
}

.cal-agenda-item {
  display: flex;
  align-items: baseline;
  gap: 8px;
  width: 100%;
  text-align: left;
  padding: 8px 10px;
  border: none;
  border-left: 3px solid var(--cal-none);
  background: var(--cal-none-bg);
  border-radius: 6px;
  cursor: pointer;
  font-size: 0.85rem;
  color: var(--color-text);
}

.cal-agenda-item:hover {
  filter: brightness(0.97);
}

.cal-agenda-time {
  font-weight: 600;
  color: var(--color-muted);
  flex: 0 0 auto;
  min-width: 60px;
  font-size: 0.78rem;
}

.cal-agenda-exams { border-left-color: var(--cal-exams); background: var(--cal-exams-bg); }
.cal-agenda-meetings { border-left-color: var(--cal-meetings); background: var(--cal-meetings-bg); }
.cal-agenda-personal { border-left-color: var(--cal-personal); background: var(--cal-personal-bg); }
.cal-agenda-school-break { border-left-color: var(--cal-school-break); background: var(--cal-school-break-bg); }
.cal-agenda-staff-leave { border-left-color: var(--cal-staff-leave); background: var(--cal-staff-leave-bg); }

/* ---- Event-detail modal (see docs/project_context.md, "Clicking an
   event") ---- */

.cal-modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(16, 24, 40, 0.45);
  z-index: 80;
}

.cal-modal-backdrop[hidden] {
  display: none;
}

.cal-modal {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: min(92vw, 440px);
  max-height: 80vh;
  overflow-y: auto;
  background: var(--color-surface);
  border-radius: var(--radius);
  padding: 18px;
  z-index: 90;
  box-shadow: 0 8px 24px rgba(16, 24, 40, 0.18);
}

.cal-modal[hidden] {
  display: none;
}

.cal-modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 12px;
  gap: 12px;
}

.cal-modal-header h3 {
  margin: 0;
  font-size: 1.05rem;
}

.cal-modal-body {
  display: flex;
  flex-direction: column;
  gap: 10px;
  font-size: 0.9rem;
}

.cal-modal-row {
  display: flex;
  gap: 10px;
}

.cal-modal-label {
  color: var(--color-muted);
  flex: 0 0 96px;
  font-weight: 600;
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  padding-top: 2px;
}

.cal-modal-value {
  flex: 1 1 auto;
  /* Long modal values (an attendee list, a normalized multi-line Outlook
     note) must wrap and never force the modal wider than the viewport. */
  min-width: 0;
  overflow-wrap: break-word;
}

/* Notes specifically may be several lines (see docs/project_context.md,
   "Outlook event-body normalization") - real line breaks the normalizer
   preserved must actually render as line breaks (`white-space: pre-wrap`,
   since the value is inserted as plain text, never HTML - see
   static/js/schedule_calendar.js::escapeHtml()), and a genuinely long
   note scrolls internally rather than growing the modal past a
   reasonable height. */
.cal-modal-notes {
  display: block;
  white-space: pre-wrap;
  max-height: 220px;
  overflow-y: auto;
}

.cal-modal-category {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 2px 9px;
  border-radius: 999px;
  font-size: 0.78rem;
  font-weight: 600;
}

.cal-modal-category-exams { background: var(--cal-exams-bg); color: var(--cal-exams); }
.cal-modal-category-meetings { background: var(--cal-meetings-bg); color: var(--cal-meetings); }
.cal-modal-category-personal { background: var(--cal-personal-bg); color: var(--cal-personal); }
.cal-modal-category-school-break { background: var(--cal-school-break-bg); color: var(--cal-school-break); }
.cal-modal-category-staff-leave { background: var(--cal-staff-leave-bg); color: var(--cal-staff-leave); }
.cal-modal-category-none { background: var(--cal-none-bg); color: var(--cal-none); }

.cal-modal-actions {
  margin-top: 14px;
  display: flex;
  gap: 8px;
}

.cal-modal-actions .btn {
  width: auto;
  margin-bottom: 0;
}

/* ---- Desktop (>=900px) - see docs/project_context.md, "Desktop-first
   design"/"Responsive behavior". The calendar becomes the visual focus
   of the available workspace width (never the narrow ~900-1100px
   content column every other page still correctly uses) with real
   event title+time chips inside each cell, and the day panel becomes a
   sticky column beside the grid rather than a block below it. ---- */
@media (min-width: 900px) {
  body.schedule-page .container {
    max-width: 1600px;
  }

  .cal-header {
    flex-wrap: nowrap;
  }

  .cal-header-title h2 {
    font-size: 1.4rem;
  }

  .cal-nav {
    order: 0;
    width: auto;
  }

  .cal-layout {
    flex-direction: row;
    align-items: flex-start;
  }

  .cal-container {
    flex: 1 1 auto;
    min-width: 0;
  }

  .cal-day-cell {
    min-height: 112px;
    padding: 6px;
  }

  .cal-day-events {
    flex-direction: column;
    gap: 2px;
  }

  /* Real title+time chips, not dots, once there's room to show them. */
  .cal-event {
    display: flex;
    align-items: baseline;
    gap: 5px;
    width: 100%;
    /* Flex items default to a min-width equal to their own content's
       intrinsic width, which silently defeats `.cal-event-title`'s
       ellipsis truncation below and lets a long title push this chip
       (and its whole day-cell column - see `.cal-day-cell`'s own
       `min-width: 0`) wider than it should ever be allowed to grow. */
    min-width: 0;
    height: auto;
    padding: 2px 6px;
    border-radius: 5px;
    border: none;
    border-left: 3px solid var(--cal-none);
    background: var(--cal-none-bg);
    color: var(--color-text);
    font-size: 0.76rem;
    text-align: left;
    cursor: pointer;
  }

  .cal-event:hover {
    filter: brightness(0.97);
  }

  .cal-event-time,
  .cal-event-title {
    display: inline;
  }

  .cal-event-time {
    flex: 0 0 auto;
    color: var(--color-muted);
    font-weight: 600;
  }

  .cal-event-title {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .cal-event-exams { border-left-color: var(--cal-exams); background: var(--cal-exams-bg); }
  .cal-event-meetings { border-left-color: var(--cal-meetings); background: var(--cal-meetings-bg); }
  .cal-event-personal { border-left-color: var(--cal-personal); background: var(--cal-personal-bg); }
  .cal-event-school-break { border-left-color: var(--cal-school-break); background: var(--cal-school-break-bg); }
  .cal-event-staff-leave { border-left-color: var(--cal-staff-leave); background: var(--cal-staff-leave-bg); }

  /* Multi-day continuation styling (see docs/project_context.md, "Multi-
     day event rendering") - a spanning event's chip is rendered
     independently in each day cell it touches (never a true cross-cell
     overlay bar, given this grid's per-cell markup), but "start"/"mid"/
     "end" segments drop the rounded corner(s) that face into the
     adjacent day so the chain of chips reads as ONE continuing event
     rather than several unrelated ones - the actual title/time text is
     only rendered on the "only"/"start" segment (see
     templates/scheduling/_calendar_grid.html) so it's never duplicated
     on every day the event spans. */
  .cal-event-seg-start,
  .cal-event-seg-mid {
    border-top-right-radius: 0;
    border-bottom-right-radius: 0;
  }

  .cal-event-seg-mid,
  .cal-event-seg-end {
    border-top-left-radius: 0;
    border-bottom-left-radius: 0;
  }

  .cal-event-more {
    display: block;
    width: 100%;
    text-align: left;
    font-size: 0.74rem;
    padding: 2px 6px;
  }

  .cal-day-panel {
    flex: 0 0 300px;
    position: sticky;
    top: 16px;
    max-height: calc(100vh - 32px);
    overflow-y: auto;
  }
}
