/* =========================================================================
   UTILITIES
   ========================================================================= */
.row-between {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 1rem;
}
.stack-gap-sm > * + * {
  margin-top: 0.5rem;
}
.stack-gap-md > * + * {
  margin-top: 1rem;
}

/* =========================================================================
   PLAYFUL MICRO-ANIMATIONS
   =========================================================================
   Small motion details that make the app feel alive without getting in
   the way. Every animation here is gated behind
   `@media (prefers-reduced-motion: no-preference)` so users who've opted
   out of motion get an instant, static UI. The *no-preference* query is
   deliberately the positive form — it defaults to "no animation" in any
   environment that doesn't explicitly enable motion, which is the safe
   accessibility posture.

   Catalogue:
     - .status-dot.live               pulsing halo while Cable is connected
     - .md-card--elevated entry       staggered fade-and-rise on first paint
     - .md-btn :active                tactile scale-down press
     - .md-chip                       small pop-in on appear
     - .md-top-app-bar__mark hover    gentle wiggle
     - [data-editor-target=savedLabel] bounce-in on the "saved" state
     - .md-breadcrumbs chevron hover  slide the chevron on link hover
*/
@media (prefers-reduced-motion: no-preference) {
  /* Live status dot — gentle breathing halo while Cable is connected. Not
     a hard blink (that reads as "broken"); a slow, confident pulse. */
  @keyframes md-status-pulse {
    0%,
    100% {
      box-shadow: 0 0 0 0 color-mix(in srgb, var(--md-sys-color-primary) 60%, transparent);
    }
    50% {
      box-shadow: 0 0 0 6px color-mix(in srgb, var(--md-sys-color-primary) 0%, transparent);
    }
  }
  .status-dot.live {
    animation: md-status-pulse 2s ease-in-out infinite;
  }

  /* Staggered card entry — list pages feel like the cards are arriving
     rather than popping. We don't animate every card forever; only the
     first 8 (covers a typical first screen) get a delay, the rest land
     with the base keyframe duration so you never wait for the tail. */
  @keyframes md-card-in {
    from {
      opacity: 0;
      transform: translateY(8px);
    }
    to {
      opacity: 1;
      transform: translateY(0);
    }
  }
  .md-card--elevated {
    animation: md-card-in 320ms var(--md-sys-motion-easing-standard) both;
  }
  .md-card--elevated:nth-of-type(1) {
    animation-delay: 0ms;
  }
  .md-card--elevated:nth-of-type(2) {
    animation-delay: 40ms;
  }
  .md-card--elevated:nth-of-type(3) {
    animation-delay: 80ms;
  }
  .md-card--elevated:nth-of-type(4) {
    animation-delay: 120ms;
  }
  .md-card--elevated:nth-of-type(5) {
    animation-delay: 160ms;
  }
  .md-card--elevated:nth-of-type(6) {
    animation-delay: 200ms;
  }
  .md-card--elevated:nth-of-type(7) {
    animation-delay: 240ms;
  }
  .md-card--elevated:nth-of-type(8) {
    animation-delay: 280ms;
  }

  /* Tactile button press — scale the button down to 96% on active. Uses a
     very short duration with a springy cubic-bezier so it feels like a
     physical press rather than a slow squish. */
  .md-btn {
    transition:
      background-color var(--md-sys-motion-duration-short) var(--md-sys-motion-easing-standard),
      box-shadow var(--md-sys-motion-duration-short) var(--md-sys-motion-easing-standard),
      transform 80ms cubic-bezier(0.34, 1.56, 0.64, 1);
  }
  .md-btn:active {
    transform: scale(0.96);
  }

  /* Chips pop in slightly — noticeable on submission status changes. */
  @keyframes md-chip-in {
    from {
      opacity: 0;
      transform: scale(0.9);
    }
    to {
      opacity: 1;
      transform: scale(1);
    }
  }
  .md-chip {
    animation: md-chip-in 200ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
  }

  /* Wiggle the brand mark on hover — a tiny bit of personality where the
     user expects "nothing happens". */
  @keyframes md-mark-wiggle {
    0%,
    100% {
      transform: rotate(0deg);
    }
    25% {
      transform: rotate(-8deg);
    }
    75% {
      transform: rotate(8deg);
    }
  }
  .md-top-app-bar__mark:hover {
    animation: md-mark-wiggle 400ms var(--md-sys-motion-easing-standard);
  }

  /* "saved Ns ago" label bounces in when the editor queue drains. We
     can't target :not(:empty) with an animation start, so the JS
     controller adds the label text and the animation runs every time
     the textContent changes (via the :has pseudo not yet widely
     supported — fall back to re-playing the animation by toggling a
     class later if needed). For now, fade the label in when it first
     becomes non-empty using the CSS transition pattern. */
  [data-editor-target="savedLabel"] {
    transition: opacity 240ms var(--md-sys-motion-easing-standard);
  }

  /* Nudge the breadcrumb chevron on hover to hint at "click to jump".
     Targeted at the ::before pseudo element that draws the separator. */
  .md-breadcrumbs__item:has(a:hover) + .md-breadcrumbs__item::before {
    transform: rotate(45deg) translate(1px, -1px);
    transition: transform 150ms var(--md-sys-motion-easing-standard);
  }
}

/* =========================================================================
   VIEW TRANSITIONS (Chrome 126+, Edge 126+, graceful no-op elsewhere)
   =========================================================================
   Turbo 8 picks up the `<meta name="view-transition" content="same-origin">`
   in the layout and drives the browser's View Transitions API on every
   cross-page visit. The browser snapshots the outgoing DOM, performs the
   navigation, then animates between the two states.

   We give four elements a stable `view-transition-name` so they're
   tracked individually and morph cleanly instead of crossfading as part
   of the page ::view-transition-old/new blobs:

     - top-app-bar      — stays exactly in place across pages
     - brand-mark       — the circular "G" badge in the app bar
     - breadcrumbs      — the trail; fades/cross-morphs gently
     - main-content     — the page body; slides in from the right on
                          forward navigation, left on back (custom pair
                          of keyframes below)

   Customize:
     ::view-transition-old(main-content)  — outgoing page body
     ::view-transition-new(main-content)  — incoming page body

   If either keyframe exceeds 250ms the whole transition feels laggy —
   keep it tight. M3 motion tokens are used for easing/duration. */
.md-top-app-bar {
  view-transition-name: top-app-bar;
}
.md-top-app-bar__mark {
  view-transition-name: brand-mark;
}
.md-breadcrumbs {
  view-transition-name: breadcrumbs;
}
main.container {
  view-transition-name: main-content;
}
/* Sidebar — persistent across navigations like the top app bar. */
.sidebar {
  view-transition-name: sidebar;
}

@keyframes md-slide-in-right {
  from {
    opacity: 0;
    transform: translateX(16px);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}
@keyframes md-slide-out-left {
  from {
    opacity: 1;
    transform: translateX(0);
  }
  to {
    opacity: 0;
    transform: translateX(-16px);
  }
}

/* Root crossfade used when the browser falls back for untagged nodes. */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: var(--md-sys-motion-duration-medium);
  animation-timing-function: var(--md-sys-motion-easing-standard);
}

/* The main content area gets a small slide-and-fade. Not a hard left/right
   swipe — just enough motion to make the page feel continuous instead of
   teleporting. */
::view-transition-old(main-content) {
  animation: md-slide-out-left 200ms var(--md-sys-motion-easing-standard) both;
}
::view-transition-new(main-content) {
  animation: md-slide-in-right 250ms var(--md-sys-motion-easing-standard) both;
}

/* The top app bar and sidebar are persistent — don't animate them at all.
   This is the whole point of giving them named `view-transition-name`s:
   they stay exactly in place across navigations, which feels way nicer
   than crossfading as part of the root blob. */
::view-transition-old(top-app-bar),
::view-transition-new(top-app-bar),
::view-transition-old(brand-mark),
::view-transition-new(brand-mark),
::view-transition-old(sidebar),
::view-transition-new(sidebar) {
  animation: none;
}

/* Breadcrumbs change between almost every page — keep their morph
   explicit so the old trail doesn't ghost over the new one. */
::view-transition-old(breadcrumbs),
::view-transition-new(breadcrumbs) {
  animation-duration: 180ms;
  animation-timing-function: var(--md-sys-motion-easing-standard);
}

/* Respect user preference — reduce everything to a near-instant crossfade
   if they've asked for reduced motion. The meta tag still triggers the
   API so Turbo's handoff stays clean. */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(root),
  ::view-transition-new(root),
  ::view-transition-old(main-content),
  ::view-transition-new(main-content),
  ::view-transition-old(breadcrumbs),
  ::view-transition-new(breadcrumbs) {
    animation-duration: 1ms;
  }
}

/* =========================================================================
   ENTRY / EXIT ANIMATIONS (Chrome 117+, Edge 117+, Safari 17.4+)
   =========================================================================
   Modern CSS lets us animate elements *into* and *out of* the DOM without
   JavaScript by combining three new features:

     1. `@starting-style { … }`     — the "before" state for entering nodes
     2. `transition-behavior: allow-discrete` — animate display/visibility
     3. transition on `display` itself

   We use this for:
     - Toasts (flash messages) — pop up, fade out
     - Cards in lists — gentle stagger as the list renders
     - Notification dropdowns — popover-style enter/leave
     - Help article cards — slide up on first paint

   Browsers without support fall back to "appears instantly" which is the
   correct no-op behaviour. */

/* Toast — uses the new `@starting-style` so the appearance is animated
   even on the very first render, without needing JS to add a class. */
@supports (transition-behavior: allow-discrete) {
  .toast {
    transition:
      opacity var(--md-sys-motion-duration-medium) var(--md-sys-motion-easing-standard),
      transform var(--md-sys-motion-duration-medium) var(--md-sys-motion-easing-standard),
      display var(--md-sys-motion-duration-medium) allow-discrete;
  }
  @starting-style {
    .toast.is-visible {
      opacity: 0;
      transform: translateX(-50%) translateY(1rem);
    }
  }
}

/* Card lists (assignments index, classes, help index, dashboard tables)
   get a tiny entry animation so newly-rendered content slides in instead
   of teleporting. */
@keyframes md-card-enter {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}
.md-card,
.dash-table tbody tr {
  animation: md-card-enter 320ms var(--md-sys-motion-easing-standard) both;
}
/* Stagger the first few cards so the page paints in a wave instead of
   all at once. After the 6th card the stagger maxes out — anything past
   that is already below the fold and would feel sluggish. */
.md-card:nth-of-type(1) {
  animation-delay: 0ms;
}
.md-card:nth-of-type(2) {
  animation-delay: 40ms;
}
.md-card:nth-of-type(3) {
  animation-delay: 80ms;
}
.md-card:nth-of-type(4) {
  animation-delay: 120ms;
}
.md-card:nth-of-type(5) {
  animation-delay: 160ms;
}
.md-card:nth-of-type(n + 6) {
  animation-delay: 200ms;
}

/* Popover-style entry/exit using @starting-style. Use this on any
   element that is conditionally rendered with `[hidden]` or whose
   `display` flips between `none` and `block`. */
@supports (transition-behavior: allow-discrete) {
  .md-popover,
  [data-popover] {
    opacity: 1;
    transform: translateY(0) scale(1);
    transition:
      opacity 200ms var(--md-sys-motion-easing-standard),
      transform 200ms var(--md-sys-motion-easing-standard),
      display 200ms allow-discrete,
      overlay 200ms allow-discrete;
  }
  .md-popover[hidden],
  [data-popover][hidden] {
    opacity: 0;
    transform: translateY(-8px) scale(0.98);
  }
  @starting-style {
    .md-popover:not([hidden]),
    [data-popover]:not([hidden]) {
      opacity: 0;
      transform: translateY(-8px) scale(0.98);
    }
  }
}

@media (prefers-reduced-motion: reduce) {
  .md-card,
  .dash-table tbody tr {
    animation: none;
  }
  .md-popover,
  [data-popover] {
    transition: none;
  }
}

/* -- Toggle switch ----------------------------------------------------- */
.md-toggle {
  appearance: none;
  width: 2.75rem;
  height: 1.5rem;
  background: var(--md-sys-color-surface-container-highest, #ddd);
  border-radius: 1rem;
  position: relative;
  cursor: pointer;
  transition: background 0.2s ease;
  flex-shrink: 0;
}
.md-toggle::before {
  content: "";
  position: absolute;
  top: 3px;
  left: 3px;
  width: 1.125rem;
  height: 1.125rem;
  background: #fff;
  border-radius: 50%;
  transition: transform 0.2s ease;
}
.md-toggle:checked {
  background: var(--md-sys-color-primary, #4f46e5);
}
.md-toggle:checked::before {
  transform: translateX(1.25rem);
}

/* -- Settings avatar --------------------------------------------------- */
.settings-avatar {
  width: 5rem;
  height: 5rem;
  border-radius: 50%;
  overflow: hidden;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--md-sys-color-primary-container, #e0e0ff);
  color: var(--md-sys-color-on-primary-container, #231f73);
}
.settings-avatar__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.settings-avatar__initials {
  font-family: var(--md-sys-typescale-font-shell, "Manrope", sans-serif);
  font-size: 1.5rem;
  font-weight: 700;
  letter-spacing: 0.05em;
}

/* -- Nav avatar (top app bar) ------------------------------------------ */
.nav-avatar {
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 50%;
  overflow: hidden;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--md-sys-color-primary-container, #e0e0ff);
  color: var(--md-sys-color-on-primary-container, #231f73);
  font-family: var(--md-sys-typescale-font-shell, "Manrope", sans-serif);
  font-size: 0.7rem;
  font-weight: 700;
  vertical-align: middle;
}
.nav-avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* =========================================================================
   NOTIFICATIONS
   ========================================================================= */

.notification-wrapper {
  position: relative;
}

.notification-bell {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: none;
  cursor: pointer;
}

.notification-badge {
  position: absolute;
  top: 0;
  right: -2px;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  border-radius: 8px;
  background: var(--md-sys-color-error);
  color: var(--md-sys-color-on-error);
  font: 600 0.5625rem/16px var(--md-sys-typescale-font-label);
  text-align: center;
  pointer-events: none;
}

.notification-panel {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  width: 360px;
  max-height: 480px;
  overflow-y: auto;
  background: var(--md-sys-color-surface-container-lowest);
  border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: var(--md-sys-shape-corner-extra-large);
  box-shadow: 0 10px 40px -10px rgba(25, 28, 30, 0.12);
  z-index: 100;
}

.notification-panel__header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 14px 16px;
  border-bottom: 1px solid var(--md-sys-color-ghost-stroke);
  font: 600 0.875rem/1.4 var(--md-sys-typescale-font-shell);
  color: var(--md-sys-color-on-surface);
}

.notification-panel__mark-all {
  font: 500 0.75rem/1 var(--md-sys-typescale-font-label);
  color: var(--md-sys-color-primary);
  background: none;
  border: none;
  cursor: pointer;
  padding: 4px 8px;
  border-radius: var(--md-sys-shape-corner-small);
  transition: background 0.15s;
}

.notification-panel__mark-all:hover {
  background: var(--md-sys-color-surface-container);
}

.notification-item {
  display: flex;
  gap: 12px;
  padding: 12px 16px;
  text-decoration: none;
  color: var(--md-sys-color-on-surface);
  border-bottom: 1px solid var(--md-sys-color-ghost-stroke);
  transition: background 0.15s;
}

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

.notification-item:hover {
  background: var(--md-sys-color-surface-container-high);
}

.notification-item--unread {
  background: var(--md-sys-color-surface-container);
}

.notification-item--unread:hover {
  background: var(--md-sys-color-surface-container-high);
}

.notification-item__icon {
  flex-shrink: 0;
  color: var(--md-sys-color-primary);
  margin-top: 2px;
}

.notification-item__body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.notification-item__message {
  font: 400 0.8125rem/1.5 var(--md-sys-typescale-font-shell);
}

.notification-item__time {
  font: 400 0.6875rem/1 var(--md-sys-typescale-font-label);
  color: var(--md-sys-color-on-surface-variant);
}

.notification-panel__empty,
.notification-panel__loading {
  padding: 32px 16px;
  text-align: center;
  color: var(--md-sys-color-on-surface-variant);
  font: 400 0.8125rem/1.4 var(--md-sys-typescale-font-shell);
}

@media (max-width: 720px) {
  .notification-panel {
    width: calc(100vw - 32px);
    right: -8px;
  }
}

/* Notification history (full page) */
.notification-history {
  display: flex;
  flex-direction: column;
  gap: 1px;
  background: var(--md-sys-color-ghost-stroke);
  border: 1px solid var(--md-sys-color-ghost-stroke);
  border-radius: var(--md-sys-shape-corner-large);
  overflow: hidden;
}
.notification-history__item {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 16px 20px;
  background: var(--md-sys-color-surface-container-lowest);
  text-decoration: none;
  color: var(--md-sys-color-on-surface);
  transition: background 0.15s;
}
.notification-history__item:hover {
  background: var(--md-sys-color-surface-container-high);
}
.notification-history__item--unread {
  background: var(--md-sys-color-surface-container);
}
.notification-history__icon {
  flex-shrink: 0;
  color: var(--md-sys-color-primary);
}
.notification-history__body {
  flex: 1;
  min-width: 0;
}
.notification-history__title {
  font: 600 0.875rem/1.4 var(--md-sys-typescale-font-shell);
  color: var(--md-sys-color-on-surface);
}
.notification-history__message {
  font: 400 0.8125rem/1.4 var(--md-sys-typescale-font-shell);
  color: var(--md-sys-color-on-surface-variant);
}
.notification-history__time {
  flex-shrink: 0;
  font: 400 0.75rem/1 var(--md-sys-typescale-font-label);
  color: var(--md-sys-color-outline);
  white-space: nowrap;
}

/* Collapsible details (student list) */
.student-list-details summary::-webkit-details-marker {
  display: none;
}
.student-list-details summary .md-icon {
  transition: transform 0.2s ease;
}
.student-list-details[open] summary .md-icon {
  transform: rotate(90deg);
}

/* =========================================================================
   PAGINATION (Pagy)
   ========================================================================= */
/* Table header row — title left, pagination right, sticky */
.dash-table-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  position: sticky;
  top: 72px; /* below the top app bar */
  z-index: 30;
  background: var(--md-sys-color-glass-heavy);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  padding: 12px 0;
  margin: 0 -1px;
  border-bottom: 1px solid var(--md-sys-color-ghost-stroke);
}
.dash-table-header h5,
.dash-table-header h2 {
  margin: 0;
}
nav.pagy {
  display: flex;
  align-items: center;
  gap: 4px;
  font-family: var(--md-sys-typescale-font-label), sans-serif;
  font-size: 0.8125rem;
  flex-shrink: 0;
}
nav.pagy a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 32px;
  height: 32px;
  padding: 0 8px;
  border-radius: var(--md-sys-shape-corner-medium);
  text-decoration: none;
  font-weight: 500;
  color: var(--md-sys-color-on-surface-variant);
  cursor: pointer;
  transition:
    background 0.15s,
    color 0.15s;
}
nav.pagy a:hover {
  background: var(--md-sys-color-surface-container);
  color: var(--md-sys-color-on-surface);
}
nav.pagy a.current {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 32px;
  height: 32px;
  padding: 0 8px;
  border-radius: var(--md-sys-shape-corner-medium);
  background: var(--md-sys-color-primary);
  color: var(--md-sys-color-on-primary);
  font-weight: 600;
  pointer-events: none;
}
nav.pagy a[aria-disabled="true"] {
  color: var(--md-sys-color-outline-variant);
  pointer-events: none;
  cursor: default;
}
