/* ==========================================================================
   ProfSchedule AI — warm academic design system
   ========================================================================== */

:root {
  /* Surfaces

     The page is paper, not the sheet lying on it. It used to be #fdf9f7 --
     two points off white and warm with it, so the cards had nothing to sit
     against and the whole screen read as cream. It is now a soft grey with
     only a trace of warmth left, ten points down from white, which is enough
     for a white card to read as a separate object.

     Not darker than this, though: --text-muted on the page background comes
     to 4.53:1 here, and AA wants 4.5. A page a couple of points deeper starts
     failing for every muted line that sits directly on it. */
  --bg: #f3f1ef;
  --bg-tint: #eae7e4;
  --surface: #ffffff;
  --surface-alt: #faf9f8;
  --border: #e5e1dd;
  /* The muted band a widget's panel is mounted on. See the Cards section.
     Its own value rather than an alias of --bg-tint: the band has to be a
     visible step down from --surface, and --bg-tint sits close enough to
     white that the frame all but disappeared against a white panel. */
  --frame-fill: #e7e3df;

  /* The screen's own cut-outs: the notch, the home indicator, and the
     rounded corners that eat the ends of anything pinned to an edge.

     Named here rather than calling env() at each site, because these are read
     from a dozen places and env() cannot be overridden -- which makes a layout
     built directly on it impossible to check anywhere but on the device. Held
     as tokens, the same layout can be put under a simulated iPhone and
     measured.

     They are zero unless the viewport meta says viewport-fit=cover. It did
     not for a long time, so every one of these sites was quietly doing
     nothing. */
  --safe-top: env(safe-area-inset-top, 0px);
  --safe-right: env(safe-area-inset-right, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --safe-left: env(safe-area-inset-left, 0px);
  --border-strong: #d3cec9;

  /* Text */
  --text: #2f2a27;
  --text-soft: #6b6058;
  /* Darkened when the surfaces became translucent: over glass this was
     2.97:1, and it was only 3.17:1 on the old opaque white, so it never
     met AA. #786b62 reaches 4.82:1 on a card and 4.63:1 on the fainter
     panels while staying clearly secondary to --text-soft. */
  --text-muted: #786b62;

  /* Brand — warm coral */
  --primary: #e0785d;
  --primary-hover: #d16849;
  --primary-soft: #fdeee8;
  --primary-softer: #fef6f3;
  --primary-ring: rgba(224, 120, 93, 0.28);

  /* Status */
  --success: #4a9d7f;
  --success-soft: #e6f4ee;
  --warning: #d9a05b;
  --warning-soft: #fdf1e3;
  --danger: #d0625c;
  --danger-soft: #fbeceb;
  --info: #6b8cae;
  --info-soft: #eaf1f7;

  /* Event categories — soft pastels */
  --cat-lecture: #e0785d;
  --cat-lecture-soft: #fdeee8;
  --cat-lab: #8b7bb8;
  --cat-lab-soft: #f0ecf9;
  --cat-meeting: #4a9d7f;
  --cat-meeting-soft: #e6f4ee;
  --cat-project_review: #d9a05b;
  --cat-project_review-soft: #fdf1e3;
  --cat-examination: #d0625c;
  --cat-examination-soft: #fbeceb;
  --cat-personal: #6b8cae;
  --cat-personal-soft: #eaf1f7;
  --cat-research: #4f9a94;
  --cat-research-soft: #e4f2f1;
  --cat-deadline: #c86b8a;
  --cat-deadline-soft: #fbecf1;
  --cat-conference: #7a9a5b;
  --cat-conference-soft: #eef4e7;
  --cat-fdp: #b5824a;
  --cat-fdp-soft: #f9efe4;
  --cat-workshop: #5b87b5;
  --cat-workshop-soft: #e9f0f7;
  --cat-other: #8b8178;
  --cat-other-soft: #f2efec;

  /* Shape */
  --radius-sm: 10px;
  --radius: 16px;
  --radius-lg: 22px;
  --radius-pill: 999px;

  /* Elevation */
  --shadow-xs: 0 1px 2px rgba(76, 52, 42, 0.04);
  --shadow-sm: 0 2px 8px rgba(76, 52, 42, 0.05);
  --shadow: 0 4px 16px rgba(76, 52, 42, 0.06);
  --shadow-md: 0 8px 26px rgba(76, 52, 42, 0.09);
  --shadow-lg: 0 18px 44px rgba(76, 52, 42, 0.13);

  /* Motion — one easing curve everywhere keeps movement coherent */
  --ease: cubic-bezier(0.4, 0, 0.2, 1);
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
  --t-fast: 140ms;
  --t: 220ms;
  --t-slow: 380ms;

  --sidebar-w: 272px;
  --rail-w: 340px;
}

* { box-sizing: border-box; }
/* Text size. 100% means "whatever the reader set in their browser", so the
   scale multiplies that rather than overriding it -- someone who has already
   raised their default keeps the benefit. */
:root { --font-scale: 1; }
:root[data-font="small"]  { --font-scale: 0.9; }
:root[data-font="medium"] { --font-scale: 1; }
:root[data-font="large"]  { --font-scale: 1.15; }

html {
  -webkit-text-size-adjust: 100%;
  font-size: calc(100% * var(--font-scale));
}

body {
  margin: 0;
  font-family: "Inter", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif;
  background: var(--bg);
  color: var(--text);
  /* Was 14px. In rem so the text-size setting reaches it too. */
  font-size: 0.875rem;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

a { color: inherit; text-decoration: none; }
button { font-family: inherit; cursor: pointer; border: none; background: none; color: inherit; }
h1, h2, h3, h4 { margin: 0; font-weight: 700; letter-spacing: -0.015em; color: var(--text); }
h2 { font-size: 1.3rem; }
h3 { font-size: 1.02rem; }
p { margin: 0; }
input, select, textarea, button { font-size: inherit; }

.hidden { display: none !important; }

/* Focus ring — visible for keyboards, quiet for mouse users */
:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
  border-radius: 6px;
}

::selection { background: var(--primary-soft); color: var(--primary-hover); }

/* Scrollbars */
::-webkit-scrollbar { width: 9px; height: 9px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 99px; }
::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }

/* ==========================================================================
   Motion
   ========================================================================== */
@keyframes fadeUp { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes scaleIn { from { opacity: 0; transform: scale(0.96); } to { opacity: 1; transform: scale(1); } }
@keyframes slideRight { from { opacity: 0; transform: translateX(-10px); } to { opacity: 1; transform: none; } }
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes pulseFade { 0%, 100% { opacity: 0.5; } 50% { opacity: 1; } }
@keyframes shimmer { 0% { background-position: -420px 0; } 100% { background-position: 420px 0; } }
@keyframes dotBounce { 0%, 80%, 100% { transform: translateY(0); opacity: 0.4; } 40% { transform: translateY(-5px); opacity: 1; } }
@keyframes wave { 0%, 60%, 100% { transform: rotate(0deg); } 20% { transform: rotate(16deg); } 40% { transform: rotate(-8deg); } }

.animate-in { animation: fadeUp var(--t-slow) var(--ease-out) both; }
/* Stagger children so a dashboard assembles rather than snapping in */
.stagger > * { animation: fadeUp var(--t-slow) var(--ease-out) both; }
.stagger > *:nth-child(1) { animation-delay: 30ms; }
.stagger > *:nth-child(2) { animation-delay: 70ms; }
.stagger > *:nth-child(3) { animation-delay: 110ms; }
.stagger > *:nth-child(4) { animation-delay: 150ms; }
.stagger > *:nth-child(5) { animation-delay: 190ms; }
.stagger > *:nth-child(6) { animation-delay: 230ms; }
/* Past the sixth the delay stops growing. A long page should not spend a
   second and a half assembling itself, and nothing below the fold is being
   watched anyway. */
.stagger > *:nth-child(n+7) { animation-delay: 230ms; }

/* A hidden section must not be handed a fill-mode that reveals it. */
.stagger > .hidden { animation: none; }

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

/* ==========================================================================
   App shell
   ========================================================================== */
.app-shell { min-height: 100vh; }
.layout { display: flex; min-height: 100vh; }

/* ---- Sidebar ---- */
.sidebar {
  width: var(--sidebar-w);
  flex-shrink: 0;
  background: var(--surface);
  border-right: 1px solid var(--border);
  padding: 22px 16px 18px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  position: sticky;
  top: 0;
  height: 100vh;
  overflow-y: auto;
  z-index: 60;
  transition: transform var(--t-slow) var(--ease-out);
}

.brand-block { display: flex; align-items: center; gap: 11px; padding: 0 8px 22px; flex-shrink: 0; }
.brand-mark {
  width: 40px; height: 40px;
  border-radius: 12px;
  background: linear-gradient(135deg, var(--primary) 0%, #eda184 100%);
  display: grid; place-items: center;
  font-size: 1.15rem;
  box-shadow: 0 4px 12px var(--primary-ring);
  flex-shrink: 0;
  transition: transform var(--t) var(--ease-spring);
}
.brand-block:hover .brand-mark { transform: rotate(-8deg) scale(1.06); }
.brand-name { font-size: 1.12rem; font-weight: 800; letter-spacing: -0.03em; line-height: 1.15; }
.brand-name span { color: var(--primary); }
.brand-tag { font-size: 0.7rem; color: var(--text-muted); letter-spacing: 0.01em; }

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

.nav-link {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-shrink: 0;
  padding: 11px 14px;
  border-radius: 13px;
  color: var(--text-soft);
  font-weight: 600;
  font-size: 0.905rem;
  width: 100%;
  text-align: left;
  position: relative;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.nav-link .nav-ico {
  width: 21px; text-align: center; font-size: 1.02rem;
  transition: transform var(--t) var(--ease-spring);
}
.nav-link:hover { background: var(--primary-softer); color: var(--text); }
.nav-link:hover .nav-ico { transform: scale(1.14); }
.nav-link:active { transform: scale(0.985); }

.nav-link.active {
  background: linear-gradient(135deg, var(--primary) 0%, #eb9375 100%);
  color: #fff;
  box-shadow: 0 6px 18px var(--primary-ring);
}
.nav-link.active::after {
  content: "›";
  margin-left: auto;
  font-size: 1.15rem;
  opacity: 0.85;
  transition: transform var(--t) var(--ease);
}
.nav-link.active:hover::after { transform: translateX(3px); }

/* --------------------------------------------------------------------------
   The travelling indicator (fluid-tabs)

   One pill per nav, moved under the active item by nav-pill.js. Position and
   size are custom properties so a single element serves items of different
   widths, and the spring is the project's own --ease-spring, which already
   overshoots slightly -- the same character as the source's stiffness 280 /
   damping 25.

   translate rather than transform, so a hover or press on the item can still
   use transform without the two fighting over one property.
   -------------------------------------------------------------------------- */
.nav-pill {
  position: absolute;
  top: 0;
  left: 0;
  width: var(--pill-w, 0);
  height: var(--pill-h, 0);
  translate: var(--pill-x, 0) var(--pill-y, 0);
  border-radius: 13px;
  opacity: 0;
  pointer-events: none;
  z-index: 0;
  /* 620ms, not 460. The pill crosses the whole sidebar on some moves and at
     the shorter time it arrived before the eye had found it leaving, which
     reads as a jump rather than as travel. */
  transition: translate 620ms var(--ease-spring),
              width 620ms var(--ease-spring),
              height 620ms var(--ease-spring),
              opacity var(--t) var(--ease);
}
.nav-pill.is-ready { opacity: 1; }
/* Placement that is not a move: first paint, a resize, a badge appearing. */
.nav-pill.is-instant { transition: none; }

/* In the sidebar the pill *is* the active state, so the static gradient is
   handed over to it rather than drawn twice. */
.sidebar .nav-pill {
  background: linear-gradient(135deg, var(--primary) 0%, #eb9375 100%);
  box-shadow: 0 6px 18px var(--primary-ring);
}
.sidebar.has-pill .nav-link.active {
  background: none;
  box-shadow: none;
}
/* The label has to sit above the pill it is riding on. */
.nav-link { z-index: 1; }

/* The source pulses the arriving tab through a short blur. That was tried
   here and rejected: it dims and blurs the label, so anything that stops the
   animation from finishing -- a background tab, a stalled frame -- leaves a
   primary navigation item unreadable. The blur is decoration; the label is
   the function, and the app already refuses that trade in the loader.

   So the arrival is carried by the icon alone, springing up to size. Frozen
   at any frame it is a slightly small icon, which costs nothing.

   Animating the `scale` property rather than `transform` leaves the active
   states' own transforms (the tab icon's lift, the sidebar icon's hover)
   intact instead of overwriting them, and no fill mode means the base value
   returns the moment it finishes. */
@keyframes navArrive {
  0%   { scale: 0.86; }
  60%  { scale: 1.05; }
  100% { scale: 1; }
}
.nav-link.is-arriving .nav-ico,
.bn-item.is-arriving .bn-ico { animation: navArrive 560ms var(--ease-spring); }

@media (prefers-reduced-motion: reduce) {
  /* The pill still marks the active item; it just stops travelling to it. */
  .nav-pill { transition: opacity var(--t) var(--ease); }
  .nav-link.is-arriving .nav-ico,
  .bn-item.is-arriving .bn-ico { animation: none; }
}

.nav-badge {
  margin-left: auto;
  background: var(--primary-soft);
  color: var(--primary-hover);
  font-size: 0.7rem;
  font-weight: 800;
  padding: 2px 8px;
  border-radius: var(--radius-pill);
  min-width: 22px;
  text-align: center;
  transition: transform var(--t) var(--ease-spring);
}
.nav-link.active .nav-badge { background: rgba(255,255,255,0.25); color: #fff; }
.nav-link:hover .nav-badge { transform: scale(1.1); }

/* AI assistant promo card */
.assistant-card {
  margin-top: auto;
  flex-shrink: 0;
  --frame: 5px;
  background: linear-gradient(160deg, var(--primary-softer) 0%, var(--primary-soft) 100%);
  background-clip: padding-box;
  border: var(--frame) solid var(--frame-fill);
  border-radius: calc(var(--radius) + var(--frame));
  padding: 10px;
  position: relative;
  overflow: hidden;
}
.assistant-title { display: flex; align-items: center; gap: 7px; font-weight: 700; font-size: 0.88rem; }
.assistant-sub { font-size: 0.75rem; color: var(--text-soft); margin: 6px 0 12px; line-height: 1.45; }
.assistant-btn {
  display: flex; align-items: center; justify-content: space-between;
  width: 100%;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 11px;
  padding: 9px 12px;
  font-size: 0.82rem;
  font-weight: 700;
  color: var(--text);
  box-shadow: var(--shadow-xs);
  transition: transform var(--t) var(--ease), box-shadow var(--t) var(--ease), background var(--t) var(--ease);
}
.assistant-btn:hover { transform: translateY(-2px); box-shadow: var(--shadow); background: #fff; }
.assistant-btn:active { transform: translateY(0); }

.sidebar-quote {
  flex-shrink: 0;
  margin-top: 16px;
  padding: 0 8px;
  font-size: 0.8rem;
  font-style: italic;
  color: var(--text-muted);
  line-height: 1.5;
}
.sidebar-quote .qmark { font-size: 1.5rem; color: var(--border-strong); line-height: 0.5; display: block; }

.logout-btn { color: var(--danger); }
.logout-btn:hover { background: var(--danger-soft); color: var(--danger); }

/* ---- Main column ---- */
.main-col { flex: 1; min-width: 0; display: flex; flex-direction: column; }

.topbar {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 16px 26px;
  /* Opaque: the page scrolls under this, and anything readable through it
     puts two lines of text in the same strip of screen. */
  background: var(--bg);
  position: sticky;
  top: 0;
  z-index: 55;
  transition: box-shadow var(--t) var(--ease), background var(--t) var(--ease);
}
.topbar.is-stuck { box-shadow: var(--shadow-sm); }

.topbar-actions { display: flex; align-items: center; gap: 12px; margin-left: auto; position: relative; }
/* No search field remains, so the topbar's only job is to hold the
   actions on the right. */
.topbar { justify-content: flex-end; }

.icon-btn {
  width: 42px; height: 42px;
  border-radius: 50%;
  background: var(--surface);
  border: 1px solid var(--border);
  display: grid; place-items: center;
  font-size: 1.05rem;
  position: relative;
  box-shadow: var(--shadow-xs);
  transition: transform var(--t) var(--ease), box-shadow var(--t) var(--ease), background var(--t) var(--ease);
}
.icon-btn:hover { transform: translateY(-2px); box-shadow: var(--shadow); background: var(--primary-softer); }
.icon-btn:active { transform: translateY(0) scale(0.96); }

.badge {
  position: absolute; top: -3px; right: -3px;
  background: var(--primary); color: #fff;
  font-size: 0.63rem; font-weight: 800;
  min-width: 19px; height: 19px;
  padding: 0 5px;
  border-radius: var(--radius-pill);
  display: grid; place-items: center;
  border: 2px solid var(--bg);
  animation: scaleIn var(--t) var(--ease-spring);
}

.user-chip {
  display: flex; align-items: center; gap: 10px;
  padding: 5px 12px 5px 5px;
  border-radius: var(--radius-pill);
  background: var(--surface);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-xs);
  transition: transform var(--t) var(--ease), box-shadow var(--t) var(--ease);
}
.user-chip:hover { transform: translateY(-1px); box-shadow: var(--shadow); }
.user-avatar {
  width: 34px; height: 34px; border-radius: 50%;
  background: linear-gradient(135deg, var(--primary) 0%, #eda184 100%);
  color: #fff; display: grid; place-items: center;
  font-weight: 800; font-size: 0.83rem; flex-shrink: 0;
}
.user-name { font-weight: 700; font-size: 0.845rem; line-height: 1.2; }
.user-role { font-size: 0.71rem; color: var(--text-muted); }

.content {
  flex: 1;
  padding: 6px 26px 34px;
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--rail-w);
  gap: 20px;
  align-items: start;
}
.content-main { min-width: 0; display: flex; flex-direction: column; gap: 20px; }
.content-rail { display: flex; flex-direction: column; gap: 20px; position: sticky; top: 84px; }
.content.full { grid-template-columns: minmax(0, 1fr); }

/* ==========================================================================
   Cards
   ========================================================================== */
/* Every widget is a panel mounted on a muted frame -- the watermelon widget-1
   shape, where the outer band's radius exceeds the inner one by exactly the
   band's width. Get that wrong and nested rounding reads as a mistake.

   The band is a thick border rather than a wrapper element, which is what
   makes it affordable: there are thirty-four cards across nineteen templates
   and none of them needed an extra div. The browser derives the inner corner
   as outer-radius minus border-width, so the arithmetic cannot drift; a
   pseudo-element would have collided with .wk-card's accent bar and scrolled
   away inside .modal-card, and a spread shadow would have eaten the grid gap.

   Padding is reduced by exactly the frame, so the content box is unchanged
   and not one card moves. This is paint, not layout. */
.card {
  --frame: 6px;
  background: var(--surface);
  /* Without this the surface would flood the border area and there would be
     no frame at all. */
  background-clip: padding-box;
  border: var(--frame) solid var(--frame-fill);
  border-radius: calc(var(--radius) + var(--frame));
  /* The hairline belongs on the outside of the frame; the panel is told apart
     from the band by colour, as in the source. */
  box-shadow: 0 0 0 1px var(--border), var(--shadow-sm);
  padding: 14px 16px;
  transition: box-shadow var(--t) var(--ease), transform var(--t) var(--ease), border-color var(--t) var(--ease);
}
/* Lift and shadow already carry the hover. Tinting border-color now would
   flood the whole six-pixel band instead of shifting a hairline. */
.card-hover:hover { transform: translateY(-3px); box-shadow: 0 0 0 1px var(--border-strong), var(--shadow-md); }

.card-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; margin-bottom: 16px;
}
.card-title { display: flex; align-items: center; gap: 9px; font-size: 1rem; font-weight: 700; }
.card-title .t-ico {
  width: 30px; height: 30px; border-radius: 9px;
  background: var(--primary-soft); color: var(--primary);
  display: grid; place-items: center; font-size: 0.9rem;
}
.link-more {
  font-size: 0.78rem; font-weight: 700; color: var(--primary);
  display: inline-flex; align-items: center; gap: 4px;
  padding: 5px 11px; border-radius: var(--radius-pill);
  transition: background var(--t) var(--ease), gap var(--t) var(--ease);
}
.link-more:hover { background: var(--primary-soft); gap: 7px; }

.grid { display: grid; gap: 18px; }
.grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }

/* ==========================================================================
   Hero / greeting
   ========================================================================== */
.hero {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-lg);
  padding: 30px 32px;
  background:
    radial-gradient(1100px 320px at 88% -12%, #fbe0d5 0%, rgba(251,224,213,0) 62%),
    linear-gradient(135deg, #fef6f2 0%, #fdeee8 52%, #fce7de 100%);
  border: 1px solid var(--border);
}
/* Decorative arcs, echoing the swirls in the reference design */
.hero::before, .hero::after {
  content: "";
  position: absolute;
  border-radius: 50%;
  border: 1.5px solid rgba(224, 120, 93, 0.16);
  pointer-events: none;
}
.hero::before { width: 340px; height: 340px; top: -160px; right: -50px; }
.hero::after  { width: 480px; height: 480px; top: -220px; right: -140px; border-color: rgba(224,120,93,0.09); }

.hero-greet { font-size: 1.85rem; font-weight: 800; letter-spacing: -0.03em; position: relative; }
/* Waves three times on arrival, then stops. It was `infinite`, which kept a
   compositor animation alive for the entire session to decorate a greeting --
   the one genuinely endless animation on the page, and the kind that costs
   battery on a phone left open on a desk. */
.hero-greet .wave-emoji {
  display: inline-block;
  transform-origin: 70% 70%;
  animation: wave 2.6s var(--ease) 3;
}
@media (prefers-reduced-motion: reduce) {
  .hero-greet .wave-emoji { animation: none; }
}
.hero-sub { color: var(--text-soft); margin-top: 5px; font-size: 0.93rem; position: relative; }

.ai-input-row {
  display: flex; gap: 10px; margin-top: 20px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  padding: 6px 6px 6px 20px;
  box-shadow: var(--shadow-sm);
  position: relative;
  transition: box-shadow var(--t) var(--ease), transform var(--t) var(--ease);
}
.ai-input-row:focus-within { box-shadow: 0 0 0 4px var(--primary-ring), var(--shadow); }
.ai-input-row input {
  flex: 1; border: none; background: transparent; outline: none;
  font-size: 0.95rem; color: var(--text); min-width: 0;
}
.ai-input-row input::placeholder { color: var(--text-muted); }
.btn-ask {
  display: inline-flex; align-items: center; gap: 7px;
  background: linear-gradient(135deg, var(--primary) 0%, #eb9375 100%);
  color: #fff; font-weight: 700; font-size: 0.9rem;
  padding: 11px 22px; border-radius: var(--radius-pill);
  box-shadow: 0 5px 15px var(--primary-ring);
  white-space: nowrap;
  transition: transform var(--t) var(--ease), box-shadow var(--t) var(--ease), filter var(--t) var(--ease);
}
.btn-ask:hover { transform: translateY(-2px); box-shadow: 0 9px 22px var(--primary-ring); filter: brightness(1.04); }
.btn-ask:active { transform: translateY(0) scale(0.98); }

.ai-examples { margin-top: 14px; display: flex; flex-wrap: wrap; gap: 9px; position: relative; }
.ai-example-chip {
  display: inline-flex; align-items: center; gap: 7px;
  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--text-soft);
  padding: 8px 15px;
  border-radius: var(--radius-pill);
  font-size: 0.815rem;
  font-weight: 600;
  box-shadow: var(--shadow-xs);
  transition: transform var(--t) var(--ease), box-shadow var(--t) var(--ease), color var(--t) var(--ease), border-color var(--t) var(--ease);
}
.ai-example-chip:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow);
  color: var(--primary);
  border-color: var(--primary);
}
.ai-example-chip:active { transform: translateY(0); }

/* ==========================================================================
   Quick actions
   ========================================================================== */
.quick-actions { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 14px; }
.qa-card {
  --frame: 4px;                /* a narrower band: these tiles are small */
  display: flex; align-items: center; gap: 11px;
  background: var(--surface);
  background-clip: padding-box;
  border: var(--frame) solid var(--frame-fill);
  border-radius: calc(var(--radius) + var(--frame));
  padding: 12px 14px;
  font-weight: 700; font-size: 0.875rem;
  box-shadow: var(--shadow-xs);
  transition: transform var(--t) var(--ease), box-shadow var(--t) var(--ease), border-color var(--t) var(--ease);
}
.qa-card .qa-ico {
  width: 32px; height: 32px; border-radius: 10px;
  display: grid; place-items: center; font-size: 0.95rem; flex-shrink: 0;
  background: var(--primary-soft); color: var(--primary);
  transition: transform var(--t) var(--ease-spring);
}
.qa-card:hover { transform: translateY(-3px); box-shadow: var(--shadow-md); border-color: var(--primary-soft); }
.qa-card:hover .qa-ico { transform: scale(1.12) rotate(-6deg); }
.qa-card:active { transform: translateY(-1px); }

/* ==========================================================================
   Stat tiles
   ========================================================================== */
.stat-card {
  --frame: 5px;
  background: var(--surface);
  background-clip: padding-box;
  border: var(--frame) solid var(--frame-fill);
  border-radius: calc(var(--radius) + var(--frame));
  padding: 13px;
  box-shadow: var(--shadow-xs);
  transition: transform var(--t) var(--ease), box-shadow var(--t) var(--ease);
}
.stat-card:hover { transform: translateY(-3px); box-shadow: var(--shadow-md); }
.stat-top { display: flex; align-items: flex-start; gap: 13px; }
.stat-ico {
  width: 44px; height: 44px; border-radius: 13px;
  display: grid; place-items: center; font-size: 1.15rem; flex-shrink: 0;
  transition: transform var(--t) var(--ease-spring);
}
.stat-card:hover .stat-ico { transform: scale(1.08) rotate(-5deg); }
/* tabular-nums so a figure that updates in place does not jiggle its
   own width as the digits change. */
.stat-value { font-size: 1.95rem; font-weight: 800; line-height: 1.05; letter-spacing: -0.035em; font-variant-numeric: tabular-nums; }
.stat-label { font-size: 0.8rem; font-weight: 600; color: var(--text-soft); }
.stat-sub { font-size: 0.71rem; color: var(--text-muted); }
.stat-foot { display: flex; align-items: flex-end; justify-content: space-between; gap: 10px; margin-top: 10px; }
.trend { font-size: 0.72rem; font-weight: 800; display: inline-flex; align-items: center; gap: 2px; }
.trend.up { color: var(--success); }
.trend.down { color: var(--danger); }
.sparkline { width: 74px; height: 24px; overflow: visible; }
.sparkline path {
  fill: none; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round;
  stroke-dasharray: 200; stroke-dashoffset: 200;
  animation: drawLine 1.1s var(--ease-out) forwards;
}
@keyframes drawLine { to { stroke-dashoffset: 0; } }

/* ==========================================================================
   Schedule timeline
   ========================================================================== */
.timeline { position: relative; }
.tl-item {
  display: grid;
  grid-template-columns: 62px 18px 1fr auto;
  gap: 12px;
  align-items: start;
  padding: 13px 10px 13px 0;
  border-radius: 12px;
  transition: background var(--t) var(--ease), transform var(--t) var(--ease);
}
.tl-item:hover { background: var(--surface-alt); transform: translateX(3px); }
.tl-time { font-size: 0.745rem; font-weight: 700; color: var(--text-soft); text-align: right; line-height: 1.5; padding-top: 1px; }
.tl-time .to { color: var(--text-muted); font-weight: 600; }
.tl-rail { position: relative; display: flex; justify-content: center; padding-top: 5px; }
.tl-dot {
  width: 11px; height: 11px; border-radius: 50%;
  border: 2.5px solid var(--surface);
  box-shadow: 0 0 0 2px currentColor;
  z-index: 1;
  transition: transform var(--t) var(--ease-spring);
}
.tl-item:hover .tl-dot { transform: scale(1.28); }
/* Connector between consecutive entries */
.tl-item:not(:last-child) .tl-rail::after {
  content: "";
  position: absolute;
  top: 20px; bottom: -22px; left: 50%;
  width: 2px; margin-left: -1px;
  background: var(--border);
}
.tl-title { font-weight: 700; font-size: 0.895rem; }
.tl-meta { font-size: 0.755rem; color: var(--text-muted); margin-top: 2px; }

.tag {
  font-size: 0.7rem; font-weight: 700;
  padding: 4px 11px; border-radius: var(--radius-pill);
  white-space: nowrap;
  transition: transform var(--t) var(--ease);
}
.tl-item:hover .tag { transform: scale(1.04); }

/* ==========================================================================
   Upcoming events
   ========================================================================== */
.ue-item {
  display: flex; align-items: center; gap: 14px;
  /* Wraps so a narrow card drops the buttons to a second line instead of
     crushing the title. row-gap keeps that second line from touching. */
  flex-wrap: wrap; row-gap: 8px;
  padding: 12px 10px 12px 0;
  border-radius: 12px;
  transition: background var(--t) var(--ease), transform var(--t) var(--ease);
}
.ue-item:hover { background: var(--surface-alt); transform: translateX(3px); }
.ue-date {
  width: 54px; height: 56px; flex-shrink: 0;
  border-radius: 13px;
  background: var(--primary-soft);
  color: var(--primary-hover);
  display: grid; place-content: center; text-align: center;
  transition: transform var(--t) var(--ease-spring), background var(--t) var(--ease);
}
.ue-item:hover .ue-date { transform: scale(1.05); background: var(--primary); color: #fff; }
.ue-mon { font-size: 0.62rem; font-weight: 800; text-transform: uppercase; letter-spacing: 0.07em; }
.ue-day { font-size: 1.22rem; font-weight: 800; line-height: 1.05; }
.ue-title { font-weight: 700; font-size: 0.885rem; }
.ue-meta { font-size: 0.755rem; color: var(--text-muted); margin-top: 1px; }

/* ==========================================================================
   Mini calendar
   ========================================================================== */
.mini-cal-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
.mini-cal-title { font-size: 1.02rem; font-weight: 800; letter-spacing: -0.02em; }
.mini-nav { display: flex; gap: 6px; }
.mini-nav button {
  width: 29px; height: 29px; border-radius: 50%;
  background: var(--bg-tint); color: var(--text-soft);
  display: grid; place-items: center; font-size: 0.85rem;
  transition: background var(--t) var(--ease), color var(--t) var(--ease), transform var(--t) var(--ease);
}
.mini-nav button:hover { background: var(--primary); color: #fff; transform: scale(1.1); }
.mini-nav button:active { transform: scale(0.95); }

.mini-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 3px; }
.mini-dow {
  text-align: center; font-size: 0.68rem; font-weight: 800;
  color: var(--text-muted); padding: 5px 0 7px;
}
.mini-day {
  aspect-ratio: 1;
  display: grid; place-items: center;
  font-size: 0.79rem; font-weight: 600;
  border-radius: 50%;
  color: var(--text-soft);
  position: relative;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.mini-day:not(.muted):hover { background: var(--primary-soft); color: var(--primary-hover); transform: scale(1.12); }
.mini-day.muted { color: var(--border-strong); }
.mini-day.today {
  background: linear-gradient(135deg, var(--primary) 0%, #eb9375 100%);
  color: #fff; font-weight: 800;
  box-shadow: 0 4px 11px var(--primary-ring);
}
.mini-day.has-event::after {
  content: ""; position: absolute; bottom: 3px; left: 50%; transform: translateX(-50%);
  width: 4px; height: 4px; border-radius: 50%; background: var(--primary);
}
.mini-day.today.has-event::after { background: #fff; }

/* ==========================================================================
   Task list
   ========================================================================== */
.task-row {
  display: flex; align-items: flex-start; gap: 11px;
  padding: 11px 8px; border-radius: 12px;
  transition: background var(--t) var(--ease);
}
.task-row:hover { background: var(--surface-alt); }
.task-check {
  width: 19px; height: 19px; border-radius: 50%;
  border: 2px solid var(--border-strong);
  flex-shrink: 0; margin-top: 2px;
  display: grid; place-items: center;
  color: transparent; font-size: 0.65rem;
  transition: background var(--t) var(--ease), border-color var(--t) var(--ease), transform var(--t) var(--ease-spring), color var(--t) var(--ease);
}
.task-check:hover { border-color: var(--primary); transform: scale(1.15); }
.task-check.done { background: var(--success); border-color: var(--success); color: #fff; }
.task-name { font-weight: 700; font-size: 0.85rem; }
.task-row.completed .task-name { text-decoration: line-through; opacity: 0.55; }
.task-due { font-size: 0.73rem; color: var(--text-muted); }

/* Priority pills */
.pill { font-size: 0.68rem; font-weight: 800; padding: 3px 10px; border-radius: var(--radius-pill); white-space: nowrap; }
.priority-low { background: var(--info-soft); color: var(--info); }
.priority-medium { background: var(--warning-soft); color: var(--warning); }
.priority-high { background: var(--danger-soft); color: var(--danger); }
.priority-urgent { background: var(--danger); color: #fff; }

/* Quick reminder progress */
.reminder-bar { height: 7px; border-radius: var(--radius-pill); background: var(--bg-tint); overflow: hidden; margin-top: 12px; }
.reminder-bar span {
  display: block; height: 100%; border-radius: var(--radius-pill);
  background: linear-gradient(90deg, var(--primary) 0%, #f0a888 100%);
  transition: width var(--t-slow) var(--ease-out);
}

/* ==========================================================================
   Buttons
   ========================================================================== */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  padding: 10px 17px;
  border-radius: 12px;
  border: 1px solid var(--border);
  background: var(--surface);
  font-weight: 700;
  font-size: 0.855rem;
  color: var(--text);
  box-shadow: var(--shadow-xs);
  white-space: nowrap;
  transition: transform var(--t) var(--ease), box-shadow var(--t) var(--ease), background var(--t) var(--ease), border-color var(--t) var(--ease), color var(--t) var(--ease);
}
.btn:hover { transform: translateY(-2px); box-shadow: var(--shadow); border-color: var(--border-strong); }
.btn:active { transform: translateY(0) scale(0.985); }
.btn:disabled { opacity: 0.55; cursor: not-allowed; transform: none !important; box-shadow: var(--shadow-xs) !important; }

.btn-primary {
  background: linear-gradient(135deg, var(--primary) 0%, #eb9375 100%);
  color: #fff; border-color: transparent;
  box-shadow: 0 4px 13px var(--primary-ring);
}
.btn-primary:hover { box-shadow: 0 8px 20px var(--primary-ring); filter: brightness(1.04); }
.btn-danger { color: var(--danger); border-color: #f3d9d7; background: var(--danger-soft); }
.btn-danger:hover { background: var(--danger); color: #fff; border-color: var(--danger); }
.btn-ghost { background: transparent; border-color: transparent; box-shadow: none; }
.btn-ghost:hover { background: var(--bg-tint); box-shadow: none; }
.btn-sm { padding: 6px 12px; font-size: 0.775rem; border-radius: 9px; }
.btn-block { width: 100%; }
.btn-row { display: flex; flex-wrap: wrap; gap: 10px; }

/* ==========================================================================
   Forms
   ========================================================================== */
.form-group { margin-bottom: 15px; }
.form-group label { display: block; font-size: 0.79rem; font-weight: 700; margin-bottom: 6px; color: var(--text-soft); }
.form-group input, .form-group select, .form-group textarea {
  width: 100%;
  padding: 10px 13px;
  border-radius: 11px;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  font-family: inherit;
  transition: border-color var(--t) var(--ease), box-shadow var(--t) var(--ease);
}
.form-group input:hover, .form-group select:hover, .form-group textarea:hover { border-color: var(--border-strong); }
.form-group input:focus, .form-group select:focus, .form-group textarea:focus {
  outline: none; border-color: var(--primary); box-shadow: 0 0 0 4px var(--primary-ring);
}
.form-group input:disabled { background: var(--bg-tint); color: var(--text-muted); }
.form-row { display: flex; gap: 13px; }
.form-row .form-group { flex: 1; min-width: 0; }
.form-error { color: var(--danger); font-size: 0.8rem; margin-top: 8px; }
.form-hint { color: var(--text-muted); font-size: 0.75rem; margin-top: 5px; display: block; }

.checkbox-row { display: flex; gap: 9px; flex-wrap: wrap; }
.checkbox-row label {
  display: inline-flex; align-items: center; gap: 6px;
  font-weight: 600; font-size: 0.82rem; color: var(--text-soft);
  background: var(--bg-tint); border: 1px solid var(--border);
  padding: 7px 13px; border-radius: var(--radius-pill);
  cursor: pointer; margin: 0;
  transition: background var(--t) var(--ease), border-color var(--t) var(--ease), color var(--t) var(--ease), transform var(--t) var(--ease);
}
.checkbox-row label:hover { border-color: var(--primary); color: var(--primary); transform: translateY(-1px); }
.checkbox-row input { accent-color: var(--primary); cursor: pointer; }
.checkbox-row input:checked + span,
.checkbox-row label:has(input:checked) { background: var(--primary-soft); border-color: var(--primary); color: var(--primary-hover); }

/* ==========================================================================
   Auth pages
   ========================================================================== */
.auth-wrap {
  min-height: 100vh;
  display: grid; place-items: center;
  padding: 26px;
  background:
    radial-gradient(760px 420px at 12% 8%, #fdeee8 0%, rgba(253,238,232,0) 62%),
    radial-gradient(760px 420px at 88% 92%, #fbe4d9 0%, rgba(251,228,217,0) 62%),
    var(--bg);
}
.auth-card {
  /* Frame, radius, padding and shadow are set with the rest of the auth
     styling at the end of this file; what stays here is the sizing and the
     scroll behaviour, which are about fitting a screen rather than about the
     look. */
  width: 100%;
  max-width: 420px;
  animation: fadeUp var(--t-slow) var(--ease-out) both;
  max-height: 94vh;
  overflow-y: auto;
}
.auth-brand { display: flex; align-items: center; gap: 11px; margin-bottom: 5px; }
.auth-card h1 { font-size: 1.32rem; }
.auth-card .subtitle { color: var(--text-soft); margin-bottom: 22px; font-size: 0.88rem; }
.auth-switch { text-align: center; margin-top: 18px; font-size: 0.845rem; color: var(--text-muted); }
.auth-switch a { color: var(--primary); font-weight: 700; }
.auth-switch a:hover { text-decoration: underline; }
.auth-divider { border: none; border-top: 1px solid var(--border); margin: 20px 0; }

/* ==========================================================================
   Weekly timetable grid
   ========================================================================== */
.timetable-grid {
  display: grid;
  grid-template-columns: 74px repeat(7, minmax(104px, 1fr));
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--surface);
  min-width: 780px;
}
.tt-header, .tt-time, .tt-cell {
  border-right: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  padding: 6px;
  min-height: 58px;
}
.tt-header {
  background: var(--bg-tint);
  font-weight: 800; text-align: center;
  padding: 12px 6px; font-size: 0.79rem;
  position: sticky; top: 0; z-index: 2;
}
.tt-time {
  font-size: 0.7rem; color: var(--text-muted); font-weight: 700;
  text-align: right; padding-top: 9px; background: var(--surface-alt);
}
.tt-cell { transition: background var(--t-fast) var(--ease); }
.tt-cell:hover { background: var(--primary-softer); }
.tt-event {
  border-radius: 9px;
  padding: 6px 9px;
  font-size: 0.735rem;
  font-weight: 700;
  margin-bottom: 3px;
  cursor: pointer;
  line-height: 1.3;
  border-left: 3px solid currentColor;
  transition: transform var(--t) var(--ease), box-shadow var(--t) var(--ease), filter var(--t) var(--ease);
}
.tt-event:hover { transform: translateY(-2px) scale(1.02); box-shadow: var(--shadow); filter: brightness(0.98); }
.tt-event:active { transform: translateY(0) scale(0.99); }
.tt-event .tt-event-time { opacity: 0.78; font-size: 0.66rem; font-weight: 600; }

/* ==========================================================================
   Calendar page
   ========================================================================== */
.calendar-toolbar { display: flex; align-items: center; gap: 10px; margin-bottom: 16px; flex-wrap: wrap; }
/* Already the fluid-tabs shape -- a muted track holding a raised chip -- so
   it only wanted the chip to travel. This is also the one tab strip in the
   app that switches without loading a page, which makes it the only place the
   slide is seen as the component intends. */
.calendar-view-switch {
  display: flex; gap: 3px; margin-left: auto;
  background: var(--bg-tint); padding: 4px; border-radius: 12px;
  position: relative;
}
.calendar-view-switch .btn { box-shadow: none; border-color: transparent; background: transparent; position: relative; z-index: 1; }
.calendar-view-switch .btn:hover { background: var(--surface); transform: none; }
.calendar-view-switch .btn.active { background: var(--surface); color: var(--primary); box-shadow: var(--shadow-xs); }
/* Once the pill is live it carries the fill, and the button keeps the colour.
   Without this the chip would be painted twice, once stationary. */
.calendar-view-switch.has-pill .btn.active { background: transparent; box-shadow: none; }
.calendar-view-switch .nav-pill {
  /* The track's radius less its padding, so the chip is concentric with it. */
  border-radius: 9px;
  background: var(--surface);
  box-shadow: var(--shadow-xs);
}

.month-grid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 7px; }
.month-cell {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  min-height: 96px;
  padding: 8px;
  font-size: 0.76rem;
  transition: border-color var(--t) var(--ease), box-shadow var(--t) var(--ease), transform var(--t) var(--ease);
}
.month-cell:hover { border-color: var(--primary); box-shadow: var(--shadow); transform: translateY(-2px); }
.month-cell .date-num { font-weight: 800; color: var(--text-soft); margin-bottom: 5px; }
.month-cell.today { border-color: var(--primary); background: var(--primary-softer); }
.month-cell.today .date-num { color: var(--primary); }

/* ==========================================================================
   Tables
   ========================================================================== */
.data-table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 0.855rem; }
.data-table th {
  text-align: left; padding: 11px 13px;
  font-size: 0.72rem; font-weight: 800; text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--text-muted); background: var(--bg-tint);
  position: sticky; top: 0;
}
.data-table th:first-child { border-radius: 10px 0 0 10px; }
.data-table th:last-child { border-radius: 0 10px 10px 0; }
.data-table td { padding: 13px; border-bottom: 1px solid var(--border); }
.data-table tbody tr { transition: background var(--t-fast) var(--ease); }
.data-table tbody tr:hover { background: var(--surface-alt); }

/* ==========================================================================
   Modals
   ========================================================================== */
.modal-backdrop {
  position: fixed; inset: 0; z-index: 200;
  background: rgba(47, 42, 39, 0.42);
  backdrop-filter: blur(4px);
  display: grid; place-items: center;
  padding: 20px;
  animation: fadeIn var(--t) var(--ease) both;
}
.modal-backdrop.hidden { display: none !important; }
.modal-card {
  --frame: 8px;
  background: var(--surface);
  background-clip: padding-box;
  border: var(--frame) solid var(--frame-fill);
  border-radius: calc(var(--radius-lg) + var(--frame));
  box-shadow: var(--shadow-lg);
  padding: 18px;
  width: 100%;
  max-width: 500px;
  max-height: 90vh;
  overflow-y: auto;
  animation: scaleIn var(--t) var(--ease-out) both;
}
.modal-card h3 { margin-bottom: 16px; font-size: 1.12rem; }
.modal-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 20px; }

/* ==========================================================================
   AI result panel
   ========================================================================== */
.ai-result-card {
  margin-top: 16px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 17px 19px;
  box-shadow: var(--shadow-sm);
  animation: fadeUp var(--t) var(--ease-out) both;
  position: relative;
}
.event-line { padding: 8px 0; border-bottom: 1px dashed var(--border); font-size: 0.875rem; }
.event-line:last-of-type { border-bottom: none; }

.ai-thinking { display: flex; align-items: center; gap: 13px; }
.ai-thinking-text { font-size: 0.885rem; font-weight: 600; color: var(--text-soft); animation: pulseFade 1.9s var(--ease) infinite; }
.ai-thinking-elapsed {
  margin-left: auto; font-size: 0.73rem; color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  background: var(--bg-tint); padding: 3px 9px; border-radius: var(--radius-pill);
}

/* ==========================================================================
   Loading
   ========================================================================== */
.spinner {
  display: inline-block; width: 16px; height: 16px;
  border: 2px solid currentColor; border-right-color: transparent;
  border-radius: 50%; animation: spin 0.66s linear infinite;
  vertical-align: -3px;
}
.spinner-lg { width: 26px; height: 26px; border-width: 2.5px; color: var(--primary); }

.btn.is-loading, button.is-loading { color: transparent !important; pointer-events: none; position: relative; }
.btn.is-loading::after, button.is-loading::after {
  content: ""; position: absolute; top: 50%; left: 50%;
  width: 15px; height: 15px; margin: -8px 0 0 -8px;
  border: 2px solid rgba(255,255,255,0.9); border-right-color: transparent;
  border-radius: 50%; animation: spin 0.66s linear infinite;
}
.btn:not(.btn-primary):not(.btn-ask).is-loading::after { border-color: var(--primary); border-right-color: transparent; }

.typing-dots span {
  display: inline-block; width: 6px; height: 6px; margin-right: 3px;
  border-radius: 50%; background: currentColor;
  animation: dotBounce 1.4s infinite ease-in-out both;
}
.typing-dots span:nth-child(2) { animation-delay: 0.16s; }
.typing-dots span:nth-child(3) { animation-delay: 0.32s; }

.skeleton {
  background: linear-gradient(90deg, var(--bg-tint) 25%, var(--surface-alt) 50%, var(--bg-tint) 75%);
  background-size: 840px 100%;
  animation: shimmer 1.5s linear infinite;
  border-radius: 8px; height: 14px; margin: 10px 0;
}
.skeleton.wide { width: 100%; }
.skeleton.half { width: 58%; }
.skeleton.short { width: 34%; }

.loading-host { position: relative; }
.loading-overlay {
  position: absolute; inset: 0; z-index: 20;
  background: rgba(255,255,255,0.76);
  backdrop-filter: blur(2px);
  display: grid; place-items: center; gap: 10px;
  border-radius: var(--radius);
  font-size: 0.85rem; color: var(--text-soft);
  animation: fadeIn var(--t-fast) var(--ease) both;
}

/* ==========================================================================
   Toasts, popovers
   ========================================================================== */
.toast-container {
  position: fixed; bottom: calc(22px + var(--safe-bottom)); right: calc(22px + var(--safe-right)); z-index: 300;
  display: flex; flex-direction: column; gap: 10px;
}
.toast {
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-left: 4px solid var(--primary);
  padding: 13px 17px;
  border-radius: 13px;
  box-shadow: var(--shadow-lg);
  font-size: 0.865rem;
  font-weight: 600;
  max-width: 340px;
  animation: slideInToast var(--t) var(--ease-spring) both;
}
@keyframes slideInToast { from { opacity: 0; transform: translateX(28px) scale(0.96); } to { opacity: 1; transform: none; } }
.toast.success { border-left-color: var(--success); }
.toast.error { border-left-color: var(--danger); }

.popover {
  /* Fixed to the viewport, not absolute to the bar. The bell is always at the
     top right, so a viewport anchor puts the panel in the same place while
     letting the mobile bottom sheet actually reach the bottom. */
  position: fixed; top: 62px; right: 18px;
  width: 330px; max-height: min(420px, calc(100vh - 90px)); overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  padding: 9px;
  z-index: 120;
  animation: scaleIn var(--t) var(--ease-out) both;
  transform-origin: top right;
}
.notif-item {
  padding: 11px 13px; border-radius: 11px; font-size: 0.845rem;
  transition: background var(--t-fast) var(--ease);
}
.notif-item:hover { background: var(--surface-alt); }
.notif-item small { color: var(--text-muted); font-size: 0.74rem; }
.notif-item.notif-empty { color: var(--text-muted); }

/* Sticky so "Clear all" stays reachable once the list starts scrolling. */
.notif-head {
  position: sticky; top: -9px; z-index: 1;
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  margin: -9px -9px 6px; padding: 11px 13px 9px;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  border-radius: var(--radius) var(--radius) 0 0;
  font-size: 0.78rem; font-weight: 700; color: var(--text-soft);
}
.notif-clear {
  border: none; background: none; padding: 3px 7px; border-radius: 8px;
  font: inherit; font-size: 0.75rem; font-weight: 700;
  color: var(--primary); cursor: pointer;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.notif-clear:hover:not(:disabled) { background: var(--primary-softer); }
.notif-clear:disabled { color: var(--text-muted); cursor: default; }



/* Registered push devices */
.device-list { margin-top: 12px; display: flex; flex-direction: column; gap: 7px; }
.device-row {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 12px; border-radius: 11px;
  background: var(--surface-alt); border: 1px solid var(--border);
  font-size: 0.82rem;
}
.device-row .dv-ico { font-size: 1.05rem; }
.device-row .dv-main { flex: 1; min-width: 0; }
.device-row .dv-name { font-weight: 600; }
.device-row .dv-name .dv-this { color: var(--primary); font-weight: 700; }
.device-row .dv-when { color: var(--text-muted); font-size: 0.74rem; }
.device-row .dv-forget {
  border: none; background: none; padding: 4px 8px; border-radius: 8px;
  font: inherit; font-size: 0.74rem; font-weight: 700;
  color: var(--text-muted); cursor: pointer;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.device-row .dv-forget:hover { background: var(--danger-soft); color: var(--danger); }
.device-empty {
  padding: 11px 13px; border-radius: 11px; font-size: 0.8rem;
  background: var(--surface-alt); border: 1px dashed var(--border);
  color: var(--text-muted);
}
.device-empty.warn {
  border-color: var(--warning); color: var(--text-soft); border-style: solid;
}

/* Deadlines */
.deadline-item {
  display: flex; justify-content: space-between; align-items: center; gap: 12px;
  padding: 11px 8px; border-radius: 11px;
  transition: background var(--t) var(--ease);
}
.deadline-item:hover { background: var(--surface-alt); }
.countdown { font-weight: 800; color: var(--danger); font-size: 0.755rem; white-space: nowrap; }

.empty-state { text-align: center; padding: 26px 16px; color: var(--text-muted); }
.empty-state .emoji { font-size: 1.9rem; display: block; margin-bottom: 9px; opacity: 0.65; }
.muted-text { color: var(--text-muted); font-size: 0.815rem; }

.section-title { display: flex; align-items: center; justify-content: space-between; gap: 14px; margin-bottom: 16px; flex-wrap: wrap; }

/* Mobile menu button, bottom tab bar + scrim */
.menu-btn { display: none; }
.bottom-nav { display: none; }
.mobile-only { display: none; }
.scrim {
  position: fixed; inset: 0; z-index: 58;
  background: rgba(47,42,39,0.38);
  backdrop-filter: blur(2px);
  animation: fadeIn var(--t-fast) var(--ease) both;
}

/* ==========================================================================
   Responsive
   ========================================================================== */
@media (max-width: 1280px) {
  :root { --rail-w: 300px; }
  .quick-actions { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

@media (max-width: 1100px) {
  .content { grid-template-columns: minmax(0, 1fr); }
  .content-rail { position: static; }
  .grid-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 900px) {
  .grid-2, .grid-3 { grid-template-columns: minmax(0, 1fr); }
  .hero { padding: 24px 22px; }
  .hero-greet { font-size: 1.5rem; }
}

@media (max-width: 768px) {
  .menu-btn { display: grid; }
  .bottom-nav { display: flex; }
  .mobile-only { display: block; }
  .sidebar {
    position: fixed; top: 0; bottom: 0; left: 0;
    transform: translateX(-100%);
    box-shadow: var(--shadow-lg);
    height: 100dvh;
  }
  .sidebar.open { transform: translateX(0); }
  .topbar { padding: 12px 15px; gap: 10px; }
  .content { padding: 4px 15px 90px; gap: 16px; }
  .content-main { gap: 16px; }
  .user-name, .user-role { display: none; }
  .user-chip { padding: 4px; }
  .quick-actions { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .grid-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .stat-value { font-size: 1.6rem; }
  .hero-greet { font-size: 1.32rem; }
  .ai-input-row { padding: 5px 5px 5px 15px; }
  .btn-ask { padding: 10px 15px; }
  /* Sides only. mobile.css places the bottom, because it loads after this
     and a second rule here setting it is how the hardcoded value there went
     unnoticed. */
  .toast-container {
    left: calc(15px + var(--safe-left));
    right: calc(15px + var(--safe-right));
  }
  .toast { max-width: none; }
  .popover { width: min(330px, calc(100vw - 30px)); right: 15px; }
  .form-row { flex-direction: column; gap: 0; }
  .modal-card { padding: 13px; border-radius: calc(var(--radius) + var(--frame)); }
}

@media (max-width: 480px) {
  .quick-actions { grid-template-columns: minmax(0, 1fr); }
  .grid-4 { grid-template-columns: minmax(0, 1fr); }
  .tl-item { grid-template-columns: 52px 16px 1fr; }
  .tl-item .tag { grid-column: 3; justify-self: start; margin-top: 5px; }
}

/* Touch devices: hover lifts feel sticky, so drop them */
@media (hover: none) {
  .card-hover:hover, .btn:hover, .qa-card:hover, .stat-card:hover,
  .ai-example-chip:hover, .icon-btn:hover, .month-cell:hover {
    transform: none;
  }
}

/* ---------- Google Sign-In ---------- */
.btn-google {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  width: 100%;
  padding: 11px 16px;
  border-radius: 12px;
  border: 1px solid var(--border-strong);
  background: var(--surface);
  font-weight: 700; font-size: 0.9rem; color: var(--text);
  box-shadow: var(--shadow-xs);
  transition: transform var(--t) var(--ease), box-shadow var(--t) var(--ease), background var(--t) var(--ease);
}
.btn-google:hover { transform: translateY(-2px); box-shadow: var(--shadow); background: var(--surface-alt); }
.btn-google:active { transform: translateY(0) scale(0.99); }
.btn-google svg { width: 18px; height: 18px; flex-shrink: 0; }

.or-divider {
  display: flex; align-items: center; gap: 12px;
  margin: 18px 0; color: var(--text-muted); font-size: 0.78rem; font-weight: 600;
}
.or-divider::before, .or-divider::after {
  content: ""; flex: 1; height: 1px; background: var(--border);
}

.connected-badge {
  display: inline-flex; align-items: center; gap: 6px;
  background: var(--success-soft); color: var(--success);
  padding: 5px 12px; border-radius: var(--radius-pill);
  font-size: 0.76rem; font-weight: 800;
}

/* ---------- First-login onboarding ---------- */
.onb-hero {
  width: 64px; height: 64px; margin: 0 auto;
  border-radius: 20px;
  background: linear-gradient(135deg, var(--primary) 0%, #eb9375 100%);
  display: grid; place-items: center; font-size: 1.7rem;
  box-shadow: 0 8px 22px var(--primary-ring);
  animation: scaleIn var(--t-slow) var(--ease-spring) both;
}
.onb-steps { display: flex; flex-direction: column; gap: 10px; margin: 20px 0 4px; }
.onb-step {
  display: flex; align-items: center; gap: 12px;
  padding: 13px 15px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--surface-alt);
  transition: border-color var(--t) var(--ease), background var(--t) var(--ease), transform var(--t) var(--ease);
}
.onb-step:hover { border-color: var(--border-strong); transform: translateY(-1px); }
.onb-step.done { background: var(--success-soft); border-color: transparent; }
.onb-ico {
  width: 38px; height: 38px; border-radius: 12px; flex-shrink: 0;
  background: var(--surface); display: grid; place-items: center; font-size: 1.05rem;
  box-shadow: var(--shadow-xs);
}
.onb-title { font-weight: 700; font-size: 0.885rem; }
.onb-sub { font-size: 0.755rem; color: var(--text-muted); margin-top: 1px; }
.onb-state { font-size: 0.76rem; font-weight: 800; color: var(--success); white-space: nowrap; }
.onb-options { display: flex; flex-direction: column; gap: 14px; margin-top: 14px; padding-top: 14px; border-top: 1px dashed var(--border); }
.onb-opt { animation: fadeUp var(--t) var(--ease-out) both; }

/* ---------- Platform guide ---------- */
.tabs { display: flex; gap: 4px; background: var(--bg-tint); padding: 4px; border-radius: 12px; margin-bottom: 16px; }
.tab-btn {
  flex: 1; padding: 9px 14px; border-radius: 9px;
  font-weight: 700; font-size: 0.85rem; color: var(--text-soft);
  transition: background var(--t) var(--ease), color var(--t) var(--ease), box-shadow var(--t) var(--ease);
}
.tab-btn:hover { color: var(--text); }
.tab-btn.active { background: var(--surface); color: var(--primary); box-shadow: var(--shadow-xs); }
.tab-panel { animation: fadeUp var(--t) var(--ease-out) both; }

.guide-note {
  background: var(--primary-softer);
  border-left: 3px solid var(--primary);
  border-radius: 10px;
  padding: 11px 14px;
  font-size: 0.83rem;
  color: var(--text-soft);
  margin-bottom: 16px;
}
.guide-steps { margin: 0; padding-left: 20px; display: flex; flex-direction: column; gap: 14px; }
.guide-steps li { font-size: 0.87rem; }
.guide-steps li::marker { color: var(--primary); font-weight: 800; }
.guide-steps strong { display: block; margin-bottom: 3px; }
.guide-steps p { font-size: 0.8rem; color: var(--text-soft); line-height: 1.55; }
.guide-steps em { color: var(--text); font-style: normal; font-weight: 600; }

.guide-alt {
  margin-top: 18px; padding-top: 16px;
  border-top: 1px dashed var(--border);
  font-size: 0.85rem;
}
.guide-alt p { font-size: 0.8rem; color: var(--text-soft); margin-top: 4px; line-height: 1.55; }
.guide-alt em { color: var(--text); font-style: normal; font-weight: 600; }

.guide-warn {
  margin-top: 12px;
  background: var(--danger-soft);
  border-radius: 10px;
  padding: 11px 13px;
  font-size: 0.79rem;
  color: var(--text);
  line-height: 1.6;
}

/* ---------- iOS install instructions ---------- */
.ios-help {
  background: var(--info-soft);
  border-radius: 12px;
  padding: 14px 16px;
  font-size: 0.82rem;
  animation: fadeUp var(--t) var(--ease-out) both;
}
.ios-help strong { display: block; margin-bottom: 8px; font-size: 0.85rem; }
.ios-help ol { margin: 0; padding-left: 20px; display: flex; flex-direction: column; gap: 6px; }
.ios-help li { color: var(--text-soft); line-height: 1.5; }
.ios-help li strong { display: inline; color: var(--text); }
.ios-help small { display: block; margin-top: 10px; color: var(--text-muted); font-size: 0.75rem; }
.ios-share { font-family: -apple-system, system-ui, sans-serif; }

/* Unread notifications stand out until the bell has been opened. */
.notif-item.unread {
  background: var(--primary-softer);
  border-left: 3px solid var(--primary);
  font-weight: 600;
}

/* Manage action beside each upcoming event */
/* The actions used to be flex-shrink:0 with only a tag and a Manage button.
   Adding Show Location pushed the row to 285px inside a 259px card, and since
   it refused to shrink it starved the text column to zero width -- every
   title and date then wrapped one word per line. The row now wraps onto its
   own line instead of squeezing the text out of existence. */
.ue-actions {
  display: flex; align-items: center; gap: 8px;
  flex-wrap: wrap; justify-content: flex-end;
  margin-left: auto;
  min-width: 0;
}
/* flex-basis, not flex:1: the text needs a width it will fight for, so the
   actions wrap below rather than the title collapsing. */
.ue-body { flex: 1 1 150px; min-width: 0; }
.loc-btn, .ue-manage, .ue-actions .tag { white-space: nowrap; }
.ue-manage {
  opacity: 0;
  transition: opacity var(--t) var(--ease), transform var(--t) var(--ease), background var(--t) var(--ease);
}
.ue-item:hover .ue-manage,
.ue-manage:focus-visible { opacity: 1; }
.ue-manage:hover { background: var(--primary-soft); color: var(--primary-hover); border-color: var(--primary); }
/* Touch devices have no hover, so the button must always be visible. */
@media (hover: none) {
  .ue-manage { opacity: 1; }
}

/* Brand logo. Falls back to the emoji mark if the asset is missing. */
.brand-logo {
  width: 42px; height: 42px;
  object-fit: contain;
  flex-shrink: 0;
  transition: transform var(--t) var(--ease-spring);
}
.brand-block:hover .brand-logo { transform: rotate(-6deg) scale(1.07); }
.brand-logo-lg { width: 52px; height: 52px; }

/* ---------- Brand logo ---------- */
.brand-logo {
  width: 42px; height: 42px;
  object-fit: contain;
  flex-shrink: 0;
  transition: transform var(--t) var(--ease-spring);
}
.brand-block:hover .brand-logo,
.lp-nav .brand-block:hover .brand-logo { transform: rotate(-6deg) scale(1.07); }
.brand-logo-lg { width: 54px; height: 54px; }

/* If logo.png hasn't been added yet, fall back to the gradient mark rather
   than showing a broken-image icon. */
.brand-logo.is-missing {
  background: linear-gradient(135deg, var(--primary) 0%, #eda184 100%);
  border-radius: 12px;
  box-shadow: 0 4px 12px var(--primary-ring);
  position: relative;
}
.brand-logo.is-missing::after {
  content: "PS";
  position: absolute; inset: 0;
  display: grid; place-items: center;
  color: #fff; font-weight: 800; font-size: 0.95rem;
}

/* ---------- Five-dot loader ---------- */
@keyframes dot5 {
  0%, 70%, 100% { transform: translateY(0) scale(0.82); opacity: 0.35; }
  35%           { transform: translateY(-7px) scale(1); opacity: 1; }
}
.dots5 {
  display: inline-flex; align-items: flex-end; gap: 5px; height: 18px;
}
.dots5 span {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--primary);
  animation: dot5 1.15s infinite ease-in-out both;
}
.dots5 span:nth-child(1) { animation-delay: 0s; }
.dots5 span:nth-child(2) { animation-delay: 0.11s; }
.dots5 span:nth-child(3) { animation-delay: 0.22s; }
.dots5 span:nth-child(4) { animation-delay: 0.33s; }
.dots5 span:nth-child(5) { animation-delay: 0.44s; }
.dots5.on-dark span { background: #fff; }
.dots5-sm span { width: 5px; height: 5px; }

/* In-button variant replaces the spinner */
.btn.is-loading::after { display: none; }
.btn.is-loading .dots5 {
  position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  height: auto;
}
.btn-primary.is-loading .dots5 span,
.btn-ask.is-loading .dots5 span { background: #fff; }

@media (prefers-reduced-motion: reduce) {
  .dots5 span { animation: none; opacity: 0.7; }
}

/* ==========================================================================
   Cursor + selection behaviour
   The I-beam over labels, cards and nav made the app feel like a document.
   UI chrome now shows the normal arrow and resists accidental selection,
   while anything genuinely clickable keeps the hand, and real text fields
   keep the caret.
   ========================================================================== */
body {
  cursor: default;
  -webkit-user-select: none;
  user-select: none;
  /* Stops the grey flash when tapping UI on mobile. */
  -webkit-tap-highlight-color: transparent;
}

/* Anything actionable gets the hand. */
a[href],
button,
.btn,
.btn-ask,
.nav-link,
.bn-item,
.icon-btn,
.qa-card,
.ai-example-chip,
.tab-btn,
.tt-event,
.tt-day-btn,
.mini-day:not(.muted),
.tl-item[data-event-id],
.task-check,
.notif-item,
.user-chip,
.month-cell,
.ue-manage,
label,
select,
summary,
[role="button"],
[data-manage],
[data-complete],
[data-act],
[data-view],
[data-tab],
[data-day] {
  cursor: pointer;
}

/* Disabled controls should not invite a click. */
button:disabled,
.btn:disabled,
.btn.is-loading,
input:disabled,
select:disabled {
  cursor: not-allowed;
}

/* Real text entry keeps the caret and normal selection. */
input,
textarea,
[contenteditable="true"] {
  cursor: text;
  -webkit-user-select: text;
  user-select: text;
}
input[type="checkbox"],
input[type="radio"],
input[type="date"],
input[type="time"],
input[type="datetime-local"],
input[type="color"],
input[type="range"],
input[type="file"],
input[type="submit"],
input[type="button"] {
  cursor: pointer;
}

/* Content worth copying stays selectable. */
.selectable,
.ai-result-card,
.event-line,
code,
pre {
  -webkit-user-select: text;
  user-select: text;
  cursor: auto;
}

/* ---------- Enable-notifications banner ---------- */
.push-banner {
  display: flex;
  align-items: center;
  gap: 13px;
  margin: 0 26px 4px;
  padding: 13px 16px;
  border-radius: var(--radius);
  background: linear-gradient(135deg, var(--primary-softer) 0%, var(--primary-soft) 100%);
  border: 1px solid var(--border);
  animation: fadeUp var(--t-slow) var(--ease-out) both;
}
.pb-ico {
  width: 38px; height: 38px; flex-shrink: 0;
  border-radius: 12px;
  background: var(--surface);
  display: grid; place-items: center;
  font-size: 1.05rem;
  box-shadow: var(--shadow-xs);
}
.pb-copy { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.pb-copy strong { font-size: 0.885rem; }
.pb-copy span { font-size: 0.775rem; color: var(--text-soft); }
.pb-close { width: 32px; height: 32px; font-size: 0.8rem; background: transparent; border: none; box-shadow: none; }
.pb-close:hover { background: rgba(0, 0, 0, 0.06); transform: none; }

@media (max-width: 768px) {
  .push-banner { margin: 0 15px 4px; flex-wrap: wrap; }
  .pb-copy { flex: 1 1 60%; }
  #pb-enable { order: 3; width: 100%; }
}


/* ==========================================================================
   Route veil: shown while signing in or out
   Both actions end in a full page load, so the gap between the click and the
   new page is dead time with nothing on screen. This covers it, and doubles
   as a lock -- the form underneath cannot be submitted twice.
   ========================================================================== */
.route-veil {
  position: fixed; inset: 0; z-index: 400;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 18px;
  background: var(--bg);
  animation: veilIn 160ms var(--ease-out) both;
}
.route-veil-spinner {
  width: 46px; height: 46px;
  border-radius: 50%;
  border: 3px solid var(--border-strong);
  border-top-color: var(--primary);
  animation: veilSpin 720ms linear infinite;
}
.route-veil-label {
  font-size: 0.92rem; font-weight: 700; color: var(--text);
  letter-spacing: -0.01em;
}
.route-veil-sub { font-size: 0.8rem; color: var(--text-muted); margin-top: -10px; }

@keyframes veilIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes veilSpin { to { transform: rotate(360deg); } }

@media (prefers-reduced-motion: reduce) {
  /* A pulse rather than a spin, and no fade -- but never nothing, because
     this is the only thing telling the professor the click registered. */
  .route-veil { animation: none; }
  .route-veil-spinner {
    animation: veilPulse 1.4s var(--ease) infinite;
    border-top-color: var(--primary);
  }
  @keyframes veilPulse { 0%, 100% { opacity: 0.35; } 50% { opacity: 1; } }
}

/* Refresh ------------------------------------------------------------------ */
#refresh-btn { font-size: 1.05rem; line-height: 1; }
#refresh-btn.is-spinning {
  animation: refreshSpin 620ms linear infinite;
  color: var(--primary);
}
@keyframes refreshSpin { to { transform: rotate(360deg); } }

@media (prefers-reduced-motion: reduce) {
  /* Still says "working", without the rotation. */
  #refresh-btn.is-spinning { animation: none; opacity: 0.55; }
}

/* Navigation in progress ----------------------------------------------------
   The tapped item carries the spinner, in its own icon's place. A general
   "loading" says the app is busy; this says which thing was tapped, which is
   what stops the second tap.
   -------------------------------------------------------------------------- */
.bn-item.is-navigating .bn-ico,
.nav-link.is-navigating .nav-ico { opacity: 0; }

.bn-item.is-navigating::after,
.nav-link.is-navigating::after {
  content: "";
  position: absolute;
  width: 18px; height: 18px;
  border-radius: 50%;
  border: 2px solid var(--border-strong);
  border-top-color: var(--primary);
  animation: veilSpin 640ms linear infinite;
}
.bn-item.is-navigating::after { top: 9px; left: 50%; margin-left: -9px; }
.nav-link.is-navigating::after { left: 14px; top: 50%; margin-top: -9px; }

.bn-item.is-navigating, .nav-link.is-navigating { color: var(--primary); }

@media (prefers-reduced-motion: reduce) {
  /* Pulse rather than spin -- but never nothing, because this is the only
     thing saying the tap registered. */
  .bn-item.is-navigating::after,
  .nav-link.is-navigating::after {
    animation: veilPulse 1.4s var(--ease) infinite;
  }
}

/* ==========================================================================
   Timetable toolbar

   One bar, at every width. Its whole layout used to live inside the phone
   breakpoint, so above 768px .tt-toolbar-wrap, .tt-week-nav and .tt-overflow
   were unstyled block elements and the controls stacked into four separate
   rows down the corner of the page.

   The week nav is a segmented control -- three buttons sharing one track --
   because they are one decision, "which week", not three unrelated actions.
   Add and the overflow sit outside it: those do something to the timetable
   rather than move around it.
   ========================================================================== */
.tt-toolbar-wrap {
  display: flex;
  /* stretch, not center, and not a hardcoded height. Whatever sets the tallest
     control -- the 44px touch minimum on a phone, the natural button height on
     a desktop -- every other control matches it. A fixed height here would
     have quietly overridden that 44px, because .tt-week-nav .btn outranks the
     plain .btn the minimum is written on. */
  align-items: stretch;
  gap: 8px;
  flex-wrap: nowrap;
  min-width: 0;
}

.tt-week-nav {
  display: flex;
  align-items: stretch;
  gap: 2px;
  padding: 3px;
  border-radius: var(--radius-pill, 999px);
  background: var(--surface-alt);
  border: 1px solid var(--border);
  min-width: 0;
}

/* Inside the track the buttons drop their own chrome -- three bordered
   buttons in a bordered container reads as boxes in a box. */
.tt-week-nav .btn {
  border: none;
  box-shadow: none;
  background: transparent;
  border-radius: var(--radius-pill, 999px);
  /* Centred inside whatever height the track ends up with. Left to their own
     padding the arrows came out 2px taller than the label, because a glyph and
     a word do not measure the same -- and a segmented control with three
     different heights in it stops reading as one control. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: background var(--t-fast) var(--ease),
              color var(--t-fast) var(--ease),
              transform var(--t-fast) var(--ease);
}
.tt-week-nav .btn:hover { background: var(--primary-softer); color: var(--primary); }
.tt-week-nav .tt-today { font-weight: 700; white-space: nowrap; }

/* The arrows lean the way they move. A press that only dims gives no sense
   of direction; this one says which way the week went. */
.tt-week-nav .tt-step:active { transform: translateX(0) scale(0.88); }
#tt-prev:active { transform: translateX(-2px) scale(0.9); }
#tt-next:active { transform: translateX(2px) scale(0.9); }

/* The overflow button fills its wrapper so it matches the rest of the bar;
   without this the wrapper stretches and the button rattles inside it. */
.tt-overflow { position: relative; flex: 0 0 auto; display: flex; }
.tt-overflow > .btn { flex: 1 1 auto; }

/* The button turns into the thing it opens, and back. Rotating a "..." a
   quarter turn is enough to say the menu is the same object, still open. */
#tt-more-btn { transition: transform var(--t) var(--ease-spring), background var(--t-fast) var(--ease); }
#tt-more-btn.is-open { transform: rotate(90deg); background: var(--primary-softer); }

.tt-overflow-menu {
  position: absolute; right: 0; top: calc(100% + 8px); z-index: 90;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 14px; box-shadow: var(--shadow-lg);
  padding: 7px; min-width: 190px;
  display: flex; flex-direction: column; gap: 3px;
  animation: menuIn 180ms var(--ease-spring) both;
  transform-origin: top right;
}
@keyframes menuIn {
  from { opacity: 0; transform: scale(0.94) translateY(-6px); }
  to   { opacity: 1; transform: none; }
}

.tt-overflow-menu .btn {
  justify-content: flex-start; border: none; box-shadow: none;
  background: transparent; width: 100%;
  transition: background var(--t-fast) var(--ease), padding-left var(--t-fast) var(--ease);
}
/* Slides a little towards the pointer instead of lifting: a menu row that
   lifts off its own menu looks detached from it. */
.tt-overflow-menu .btn:hover {
  background: var(--bg-tint);
  transform: none;
  padding-left: 16px;
}

@media (prefers-reduced-motion: reduce) {
  .tt-overflow-menu { animation: none; }
  #tt-more-btn.is-open { transform: none; }
  .tt-week-nav .tt-step:active,
  #tt-prev:active, #tt-next:active { transform: none; }
  .tt-overflow-menu .btn:hover { padding-left: 12px; }
}

/* A rule between deleting a term and dating the next one. They are adjacent
   because they happen together, and separated because one is destructive and
   the other is not. */
.tt-reset-sep {
  height: 1px;
  background: var(--border);
  margin: 16px 0 14px;
}

/* ==========================================================================
   Module switch

   Two labelled halves rather than a toggle. A toggle shows one state and makes
   you infer the other; these are two places, and each half says which. The one
   you are in is filled rather than merely marked, so the current module reads
   at a glance instead of being decoded from a subtle border.
   ========================================================================== */
.mode-switch {
  display: inline-flex;
  align-items: stretch;
  gap: 2px;
  padding: 3px;
  margin-right: auto;
  border-radius: var(--radius-pill, 999px);
  background: var(--surface-alt);
  border: 1px solid var(--border);
}

.mode-opt {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 0 13px;
  min-height: 30px;
  border-radius: var(--radius-pill, 999px);
  color: var(--text-soft);
  text-decoration: none;
  font-size: 0.8rem;
  font-weight: 700;
  white-space: nowrap;
  position: relative;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.mode-opt:hover { color: var(--text); background: var(--primary-softer); }
.mode-opt.is-on {
  background: var(--surface);
  color: var(--primary);
  box-shadow: var(--shadow-sm, 0 1px 2px rgba(0, 0, 0, 0.08));
}
.mode-opt:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

.mode-badge {
  min-width: 16px; height: 16px; padding: 0 4px;
  border-radius: 999px;
  background: var(--primary); color: #fff;
  font-size: 0.62rem; font-weight: 800;
  display: inline-flex; align-items: center; justify-content: center;
}


/* ==========================================================================
   Animated link hover effects

   The five effects from skiper-ui's Skiper40, written for this project.

   They arrived as a React + Tailwind + framer-motion component, none of which
   this app has -- and none of which they need: every one of them is a CSS
   transition on a pseudo-element. The React was packaging, not substance.

   Opt-in. Nothing changes until a class is added to a link, so these cost
   nothing but the bytes until something uses them.

       <a class="lnk lnk-wipe" href="...">Read the report</a>

   All five use the project's own timing and colour tokens rather than the
   original's hard-coded values, so they sit inside the design system instead
   of beside it, and follow the theme in light and dark without a second rule.
   ========================================================================== */
.lnk {
  position: relative;
  display: inline-block;
  color: inherit;
  text-decoration: none;
  /* The underlines are pseudo-elements positioned against this, and an inline
     box gives them nothing stable to sit on. */
  padding-bottom: 1px;
}
.lnk:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 3px;
  border-radius: 3px;
}

/* --- 1. Underline wiping in from the left, out to the right --------------
   The exit is not the entrance reversed. Reversing it makes the line retreat
   the way it came, which reads as undo; carrying on in the same direction
   reads as the pointer having passed through. */
.lnk-wipe::after {
  content: "";
  position: absolute;
  left: 0; bottom: 0;
  width: 100%; height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: right;
  transition: transform var(--t) var(--ease);
}
.lnk-wipe:hover::after,
.lnk-wipe:focus-visible::after {
  transform: scaleX(1);
  transform-origin: left;
}

/* --- 2. Underline opening from the centre ------------------------------- */
.lnk-centre::after {
  content: "";
  position: absolute;
  left: 50%; bottom: 0;
  width: 0; height: 1px;
  background: currentColor;
  transition: width var(--t) var(--ease-out), left var(--t) var(--ease-out);
}
.lnk-centre:hover::after,
.lnk-centre:focus-visible::after { width: 100%; left: 0; }

/* --- 3. Arrow rising into place -----------------------------------------
   The arrow occupies its space from the start, hidden rather than absent.
   Adding it on hover would reflow the line and nudge everything after it. */
.lnk-arrow::after {
  content: "↗";
  display: inline-block;
  margin-left: 4px;
  opacity: 0;
  transform: translateY(3px);
  transition: opacity var(--t) var(--ease), transform var(--t) var(--ease-out);
}
.lnk-arrow:hover::after,
.lnk-arrow:focus-visible::after { opacity: 1; transform: translateY(0); }

/* --- 4. Background filling upward ---------------------------------------
   mix-blend-mode: difference inverts the text against its own fill, so this
   stays legible on any background without being told what that background is.
   The original hard-coded black on white and broke in dark mode. */
.lnk-fill {
  padding: 0 4px;
  isolation: isolate;
}
.lnk-fill::before {
  content: "";
  position: absolute;
  inset: 0;
  background: currentColor;
  transform: scaleY(0);
  transform-origin: bottom;
  transition: transform var(--t) var(--ease-out);
  z-index: -1;
}
.lnk-fill:hover,
.lnk-fill:focus-visible { mix-blend-mode: difference; }
.lnk-fill:hover::before,
.lnk-fill:focus-visible::before { transform: scaleY(1); }

/* --- 5. The label sliding up to a copy of itself ------------------------
   Two stacked copies inside a clipped box. The duplicate is aria-hidden in
   the markup so a screen reader hears the label once:

       <a class="lnk lnk-swap" href="..."><span data-text="Open">Open</span></a>
*/
.lnk-swap { overflow: hidden; vertical-align: bottom; }
.lnk-swap > span {
  display: inline-block;
  position: relative;
  transition: transform var(--t) var(--ease-out);
}
.lnk-swap > span::after {
  content: attr(data-text);
  position: absolute;
  left: 0; top: 100%;
  color: var(--primary);
}
.lnk-swap:hover > span,
.lnk-swap:focus-visible > span { transform: translateY(-100%); }

@media (prefers-reduced-motion: reduce) {
  /* The states remain, the travel does not. Removing the effect entirely
     would take the hover feedback with it; what people asked to be spared is
     the movement. */
  .lnk-wipe::after,
  .lnk-centre::after,
  .lnk-arrow::after,
  .lnk-fill::before,
  .lnk-swap > span { transition: none; }
}


/* ==========================================================================
   The loading animation

   Fifteen overlapping ellipses whose widths oscillate out of phase, so the row
   pinches toward the middle and opens out again -- a standing wave rather than
   anything travelling. Colour runs symmetrically from dark maroon at the edges
   through red, orange and amber, to cyan at dead centre.

   Rebuilt from the Figma prototype rather than exported: it is fifteen shapes
   and one keyframe, which is smaller as CSS than as any asset, scales to any
   size from a single variable, and needs no image request at the exact moment
   the page is already waiting on something.

   The ellipses are positioned absolutely, and that is the whole trick. A flex
   row does not work here: scaleX changes what is painted and not what is laid
   out, so the pinched inner ellipses still reserve their full width and the
   cluster comes out spread across the screen with gaps between the slivers.
   Fixing their centres and letting the paint overflow is what makes them
   overlap.

   `translate` carries the position and `transform` carries the animation --
   two separate properties, so the keyframes never have to restate an offset
   they do not care about.
   ========================================================================== */
.loader {
  --loader-size: 84px;
  --loader-step: 0.164;            /* centre-to-centre, as a fraction of size */
  position: relative;
  display: block;
  height: var(--loader-size);
  width: calc(var(--loader-size) * 3.3);
  pointer-events: none;
}

.loader > span {
  position: absolute;
  top: 0;
  left: 50%;
  width: var(--loader-size);
  height: var(--loader-size);
  margin-left: calc(var(--loader-size) / -2);
  border-radius: 50%;
  translate: calc(var(--loader-size) * var(--x)) 0;
  /* Stacked from the centre outward, so the bright slivers sit on top and the
     two halves mirror each other. Left to document order the fifteenth ellipse
     paints over all the others and the right-hand side reads as a solid blob
     while the left reads as a fan -- the same shape looking like two different
     ones. */
  z-index: var(--z, 1);
  animation: loaderPinch 1.9s cubic-bezier(0.45, 0, 0.55, 1) infinite;
  animation-delay: var(--delay, 0s);
  will-change: transform;
}

/* One keyframe for all fifteen; the per-ellipse custom properties and the
   phase offset do the rest. Fifteen separate animations would be fifteen
   things to keep in step. */
@keyframes loaderPinch {
  0%, 100% { transform: scaleX(var(--wide)); }
  50%      { transform: scaleX(var(--thin)); }
}

/* Position, colour and amplitude by distance from the centre. Written out
   rather than generated: there are fifteen, and a reader should be able to see
   the ramp instead of reconstructing it from arithmetic. */
.loader > span:nth-child(1)  { --x: -1.148; background: #8C3A32; --wide: 1.00; --thin: 0.90; --delay: 0s;  --z: 1;}
.loader > span:nth-child(15) { --x:  1.148; background: #8C3A32; --wide: 1.00; --thin: 0.90; --delay: 0s;  --z: 1;}
.loader > span:nth-child(2)  { --x: -0.984; background: #E03127; --wide: 0.92; --thin: 0.70; --delay: -0.09s;  --z: 2;}
.loader > span:nth-child(14) { --x:  0.984; background: #E03127; --wide: 0.92; --thin: 0.70; --delay: -0.09s;  --z: 2;}
.loader > span:nth-child(3)  { --x: -0.820; background: #F2431B; --wide: 0.80; --thin: 0.52; --delay: -0.18s;  --z: 3;}
.loader > span:nth-child(13) { --x:  0.820; background: #F2431B; --wide: 0.80; --thin: 0.52; --delay: -0.18s;  --z: 3;}
.loader > span:nth-child(4)  { --x: -0.656; background: #FA6A0A; --wide: 0.66; --thin: 0.36; --delay: -0.27s;  --z: 4;}
.loader > span:nth-child(12) { --x:  0.656; background: #FA6A0A; --wide: 0.66; --thin: 0.36; --delay: -0.27s;  --z: 4;}
.loader > span:nth-child(5)  { --x: -0.492; background: #FB9B06; --wide: 0.52; --thin: 0.24; --delay: -0.36s;  --z: 5;}
.loader > span:nth-child(11) { --x:  0.492; background: #FB9B06; --wide: 0.52; --thin: 0.24; --delay: -0.36s;  --z: 5;}
.loader > span:nth-child(6)  { --x: -0.328; background: #FDC000; --wide: 0.38; --thin: 0.14; --delay: -0.45s;  --z: 6;}
.loader > span:nth-child(10) { --x:  0.328; background: #FDC000; --wide: 0.38; --thin: 0.14; --delay: -0.45s;  --z: 6;}
.loader > span:nth-child(7)  { --x: -0.164; background: #2FA8E8; --wide: 0.16; --thin: 0.05; --delay: -0.54s;  --z: 7;}
.loader > span:nth-child(9)  { --x:  0.164; background: #2FA8E8; --wide: 0.16; --thin: 0.05; --delay: -0.54s;  --z: 7;}
.loader > span:nth-child(8)  { --x:  0;     background: #1E7FD0; --wide: 0.11; --thin: 0.04; --delay: -0.63s;  --z: 8;}

/* Small enough to sit beside a line of text. Below roughly this the ellipses
   stop being separable and it reads as a smudge, which is why buttons keep
   their ring rather than shrinking this into one. */
.loader-sm { --loader-size: 34px; }
.loader-lg { --loader-size: 110px; }

.loader-label {
  display: block;
  margin-top: 10px;
  text-align: center;
  font-size: 0.86rem;
  font-weight: 650;
  color: var(--text-muted);
}

/* Inline with text rather than a block of its own. */
.loader-inline { display: inline-flex; align-items: center; gap: 12px; }
.loader-inline .loader-label { margin-top: 0; }

@media (prefers-reduced-motion: reduce) {
  /* The shape stays and stops moving, so a page still looks like itself to
     somebody who asked not to be moved. Held at its widest rather than
     mid-pinch: the pinched state is a row of thin lines, and a thin line that
     never resolves reads as a rendering fault. */
  .loader > span { animation: none; transform: scaleX(var(--wide)); }
}

/* A container waiting on its first render. Centred, with room to breathe, so
   the animation is not jammed against the top of an empty card. */
.is-loading-block {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 26px 0;
}


/* ==========================================================================
   Auth pages

   Ported from the watermelon.sh auth-01 layout: a centred card built as two
   nested rounded boxes -- an outer frame with a little padding and a slightly
   tighter inner surface -- with iconed fields, a gradient submit and a
   borderless social button.

   The nesting is the whole look. One box with one radius is a plain card; two,
   with the inner radius smaller than the outer by exactly the padding between
   them, reads as a mounted panel. Getting that arithmetic wrong is what makes
   nested rounding look like a mistake, so the padding is a variable and the
   inner radius is calculated from it rather than typed twice.
   ========================================================================== */
.auth-card {
  --frame: 8px;
  background: var(--surface-alt);
  border: 1px solid var(--border);
  border-radius: calc(var(--radius-lg) + var(--frame));
  padding: var(--frame);
  box-shadow: var(--shadow-lg);
}

.auth-panel {
  background: var(--surface);
  border-radius: var(--radius-lg);
  padding: 26px 24px 24px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12);
}

.auth-card h1 {
  font-size: 1.42rem;
  font-weight: 800;
  letter-spacing: -0.02em;
  margin-top: 16px;
}

/* --- Fields with an icon -------------------------------------------------
   The icon sits inside the field rather than beside the label, which is what
   lets the two rows read as one object instead of four stacked things. */
.field { position: relative; }
.field > .field-icon {
  position: absolute;
  left: 12px;
  top: 50%;
  translate: 0 -50%;
  font-size: 0.95rem;
  line-height: 1;
  opacity: 0.55;
  pointer-events: none;
}
.field > input { padding-left: 36px !important; }

/* The muted fill belongs to every field in the panel, not only the ones with
   an icon. Registration has a timezone box and two selects that took no icon,
   and tinting only the iconed three made the form look like two different
   forms stacked on top of each other. Checkboxes are excluded: they draw their
   own box and a fill behind it just smudges the tick. */
.auth-panel input:not([type="checkbox"]):not([type="radio"]),
.auth-panel select {
  background: var(--bg-tint);
}
/* Room for the reveal button, so a long password never runs under it. */
.field.has-toggle > input { padding-right: 42px !important; }

.field-toggle {
  position: absolute;
  right: 4px;
  top: 50%;
  translate: 0 -50%;
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 9px;
  background: transparent;
  color: var(--text-muted);
  font-size: 0.95rem;
  cursor: pointer;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.field-toggle:hover { background: var(--bg-tint); color: var(--text); }
.field-toggle:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }

/* --- Submit --------------------------------------------------------------
   A vertical gradient rather than a flat fill: it is the only gradient on the
   page, which is what makes it read as the one thing to press. */
.auth-card .btn-primary.btn-block {
  height: 46px;
  background: linear-gradient(180deg, var(--primary), var(--primary-hover));
  border: 0;
  font-weight: 750;
  letter-spacing: -0.005em;
}

/* --- Social --------------------------------------------------------------
   One provider, so one full-width button. The source lays three out in a
   three-column grid; a single button in a three-column grid is a third of a
   button and two empty cells. */
.auth-card .btn-google {
  height: 44px;
  background: var(--bg-tint);
  border: 0;
  border-radius: var(--radius-md, 12px);
  font-weight: 650;
  box-shadow: none;
}
.auth-card .btn-google:hover {
  background: var(--border);
  transform: none;
  box-shadow: none;
}

.auth-card .or-divider {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-muted);
}

/* Registration asks for timings, lunch and working days, which do not fit the
   sign-in card's width without every time field wrapping onto its own line.
   Same frame, sized for what it holds. */
.auth-card.auth-card-wide { max-width: 520px; }


/* ==========================================================================
   Tap glow

   Skiper86's shape: the Apple Intelligence border, which is a glow around the
   edge of the whole screen rather than an outline on one control. Here it is
   the answer to pressing anything -- press a button and the page itself
   acknowledges it.

   It began as a ring hugging each control, which was the wrong reading of the
   component and had to measure a rectangle, follow it, and give up when the
   page scrolled underneath. Framing the viewport instead removes all of that:
   there is one shape, it is always in the right place, and nothing about the
   page beneath it matters.

   The colours are the application's own -- coral, amber, green, violet --
   rather than the source's Apple spectrum. A rainbow is the one thing in this
   palette that would read as belonging to a different program.

   The source also plays a sound on each trigger. That is not carried over:
   this fires on every press in the app, and a scheduling tool used in a
   lecture theatre should not chirp each time somebody taps it.
   ========================================================================== */
@property --tap-angle {
  syntax: "<angle>";
  initial-value: 0deg;
  inherits: false;
}

.tap-glow {
  position: fixed;
  inset: 0;
  /* Above a dialog, so pressing something inside one is still answered; below
     the toasts and the route veil, which are messages rather than decoration. */
  z-index: 250;
  pointer-events: none;
  /* How far in from the edge the light reaches. In vmin, so it claims the
     same share of a phone screen as of a desktop one -- a fixed pixel reach
     would be a hairline on one and a wash on the other.

     Cut to a quarter of what it was: at 20vmin the light came a fifth of the
     way across the screen from every side, which on a page of dense timetable
     rows is competing with the content rather than framing it. */
  --tap-reach: 5vmin;
  background: conic-gradient(
    from var(--tap-angle, 0deg),
    var(--primary), var(--warning), var(--success), var(--cat-lab), var(--primary)
  );
  /* One gradient per edge, added together: solid where it meets the edge of
     the screen and thinning to nothing on the way in. The falloff is the mask
     rather than a blur over a hard-edged band -- a band still has an inner
     edge however much it is blurred, and that edge is the line this was asked
     to lose. Where the four overlap in the corners the alpha adds, which is
     what lights the corners.

     The stops curve rather than ramping straight: over half the brightness is
     spent in the first third and the rest is a long tail, so it reads as
     something glowing at the edge instead of a gradient painted near it. */
  -webkit-mask:
    linear-gradient(to bottom, #000 0, rgba(0,0,0,0.55) 30%, rgba(0,0,0,0.16) 62%, transparent 100%) top / 100% var(--tap-reach) no-repeat,
    linear-gradient(to top, #000 0, rgba(0,0,0,0.55) 30%, rgba(0,0,0,0.16) 62%, transparent 100%) bottom / 100% var(--tap-reach) no-repeat,
    linear-gradient(to right, #000 0, rgba(0,0,0,0.55) 30%, rgba(0,0,0,0.16) 62%, transparent 100%) left / var(--tap-reach) 100% no-repeat,
    linear-gradient(to left, #000 0, rgba(0,0,0,0.55) 30%, rgba(0,0,0,0.16) 62%, transparent 100%) right / var(--tap-reach) 100% no-repeat;
  -webkit-mask-composite: source-over;
  mask:
    linear-gradient(to bottom, #000 0, rgba(0,0,0,0.55) 30%, rgba(0,0,0,0.16) 62%, transparent 100%) top / 100% var(--tap-reach) no-repeat,
    linear-gradient(to top, #000 0, rgba(0,0,0,0.55) 30%, rgba(0,0,0,0.16) 62%, transparent 100%) bottom / 100% var(--tap-reach) no-repeat,
    linear-gradient(to right, #000 0, rgba(0,0,0,0.55) 30%, rgba(0,0,0,0.16) 62%, transparent 100%) left / var(--tap-reach) 100% no-repeat,
    linear-gradient(to left, #000 0, rgba(0,0,0,0.55) 30%, rgba(0,0,0,0.16) 62%, transparent 100%) right / var(--tap-reach) 100% no-repeat;
  mask-composite: add;
  /* Only to soften the colour steps in the wheel, now that the mask owns the
     falloff. A heavy blur across the whole viewport was the expensive part of
     this and it is no longer doing the work. */
  filter: blur(7px);
  opacity: 0;
  /* Two animations, because the two halves want opposite curves. The fade
     should arrive quickly and leave slowly; the turn has to be even or it is
     not read as a turn at all. Sharing one ease-out put the ring most of the
     way round a quarter of the way in -- a flick, and then a crawl.

     Slow: at two thirds of a second this read as a flicker at the edge of
     vision rather than as something turning. It is a full turn, and a full
     turn needs long enough to be seen as one. */
  animation: tapGlowFade 1400ms linear, tapGlowSpin 1400ms linear;
  will-change: opacity;
}

/* Shaped here rather than by a timing function. An ease-out on this had the
   glow down to six percent by the halfway point, so the back half of the turn
   played to nobody -- the curve was doing the fade twice, once in the stops
   and once in itself. Linear, and the stops say when. */
@keyframes tapGlowFade {
  0%   { opacity: 0; }
  10%  { opacity: 0.9; }
  60%  { opacity: 0.72; }
  100% { opacity: 0; }
}

@keyframes tapGlowSpin {
  from { --tap-angle: 0deg; }
  to   { --tap-angle: 360deg; }
}

@media (prefers-reduced-motion: reduce) {
  /* The press still needs an answer, so the edge still lights. It just does
     not turn, and it is over sooner. */
  .tap-glow { animation: tapGlowFade 700ms linear; }
}


/* ==========================================================================
   Parallax on the dashboard hero

   The same effect as the ambient field behind the app, given somewhere it is
   plainly visible. The hero already carries two decorative arcs; on a page
   that scrolls they now drift against it, so the greeting sits in front of
   something rather than on top of a picture of something.

   view() and not scroll(): the arcs should move while the hero is on screen
   and be still otherwise, which is a question about this element's position in
   the viewport rather than about the document's total scroll.

   The hero clips its overflow, so the arcs cannot escape it however far they
   travel.
   ========================================================================== */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .hero::before,
    .hero::after {
      animation: heroArcDrift linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 100%;
    }

    /* The two arcs move at different rates. Moving together they would read as
       one object sliding; apart, they read as two at different depths, which
       is the whole point of a parallax. */
    @keyframes heroArcDrift {
      from { translate: 0 -26px; }
      to   { translate: 0 26px; }
    }
    .hero::after {
      animation-name: heroArcDriftFar;
    }
    @keyframes heroArcDriftFar {
      from { translate: 0 -12px; }
      to   { translate: 0 12px; }
    }
  }
}


/* ==========================================================================
   Cards and rows arriving as you reach them

   The scroll reveal from the landing page, brought inside the application: the
   quick actions and the week's figures on the dashboard, and -- in work.css,
   next to their own styles -- the community and task rows.

   One keyframe for all of them, with the distance a custom property, because
   a row is not a card. Twenty-six pixels under a stat tile reads as it
   settling into place; the same under a fifty-pixel task row is the row
   arriving from off its own list.

   `translate` and not `transform`. Every one of these lifts or slides on
   hover, and a filled animation holds its final value for good -- on transform
   it would quietly win every hover from then on.

   Everything is inside @supports, and that is load-bearing rather than
   polite. The reveal starts at opacity 0: if the rule applied where the
   timeline is not supported -- Firefox, older Safari -- the task list, the
   community list and the week's figures would all be permanently blank.
   ========================================================================== */
@keyframes revealUp {
  from { opacity: 0; translate: 0 var(--reveal-rise, 26px); }
  to   { opacity: 1; translate: 0 0; }
}

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .qa-card,
    .stat-card {
      animation: revealUp linear both;
      animation-timeline: view();
      /* Starts as it clears the fold, done before it reaches the middle of
         the screen. A reveal still moving at eye level reads as a page that
         has not finished loading. */
      animation-range: entry 4% cover 42%;
    }
  }
}


/* ==========================================================================
   The busy veil

   Shown when something pressed has not finished in a moment: the page behind
   it goes soft and the application's own loader sits in the middle of it.

   Blurred rather than covered. The page is still there -- what was being read
   is where it was, and comes back into focus rather than reappearing -- and
   at a threshold this short that difference is the whole feel of it. The
   route veil paints over the page instead, which is right for a page that is
   about to be replaced and wrong for a wait that is about to end.

   It does not take the pointer. A veil that blocks input is only as safe as
   the code that removes it, and there is no failure here that should be able
   to lock somebody out of the application -- if it ever outlived its work,
   the worst it can do is look wrong.
   ========================================================================== */
.busy-veil {
  position: fixed;
  inset: 0;
  /* Over the page and the toasts near it, under the route veil that
     supersedes it. */
  z-index: 310;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;

  -webkit-backdrop-filter: blur(9px) saturate(0.92);
  backdrop-filter: blur(9px) saturate(0.92);
  /* A wash of the page's own colour, so the softening reads as the page
     receding rather than as a grey sheet over it. The flat value first for
     anything without color-mix. */
  background: rgba(0, 0, 0, 0.16);
  background: color-mix(in srgb, var(--bg) 42%, transparent);

  pointer-events: none;
  animation: busyVeilIn 200ms var(--ease-out) both;
}

.busy-veil .loader-label {
  font-size: 0.9rem;
  font-weight: 700;
  color: var(--text);
}

@keyframes busyVeilIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .busy-veil { animation: none; }
}
