:root {
  --bg: #f7f7fb;
  --surface: #ffffff;
  --surface-muted: #f0f1f5;
  --text: #1c1c24;
  --text-muted: #6b6b7a;
  --border: #e2e2ea;
  --primary: #4f46e5;
  --primary-contrast: #ffffff;
  --danger: #c0392b;
  --radius: 8px;
}

* {
  box-sizing: border-box;
}

/* App-wide: no selection highlight/callout UI on tap, long-press, or
   click-drag anywhere - both the gesture-surface problem (a long-press on
   something like the screensaver's clock triggering the browser's own
   "select text" highlight/callout instead of just running the custom
   gesture) and the plain-text problem (a long-press on ordinary page text
   triggering Android Chrome's own text-selection UI, and with it its
   "Tippen, um Suchergebnisse zu sehen"/"tap to see search results" bar)
   used to be two separate, narrower rules (a .noselect class applied only
   to specific gesture surfaces, plus a mobile-only version of this same
   body * rule) - consolidated into this one, unconditional, since the
   outcome wanted is "nowhere in the app", not "only these spots" or
   "only on mobile" specifically.
   Two deliberate exceptions immediately below: form fields (typing/
   selecting-to-edit needs to keep working) and .selectable (an explicit
   opt-in for the rare bit of rendered text someone's actually meant to
   copy, e.g. an invite link - see .selectable further down). */
body * {
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
}
/* Safety net: an ancestor's user-select: none isn't universally guaranteed
   to leave a form control's own internal text (what you're actively
   typing/editing/selecting-to-edit inside it) alone across every browser -
   explicit is safer than relying on that. */
input, textarea, [contenteditable] {
  -webkit-user-select: text;
  -moz-user-select: text;
  -ms-user-select: text;
  user-select: text;
}
/* Opt-in for the rare bit of *rendered* (non-input) text someone's
   actually meant to select and copy - e.g. an invite link (see
   SettingsView.js's .invite-url). Everything else stays covered by the
   blanket rule above; this is only for genuine, deliberate exceptions. */
.selectable {
  -webkit-user-select: text;
  -moz-user-select: text;
  -ms-user-select: text;
  user-select: text;
  -webkit-touch-callout: default;
}

/* Stops an overscroll at the top/bottom from chaining into the browser's
   own pull-to-refresh / bounce-navigate gesture - relevant now that the
   nav's own drag gestures (navBar.js) and the top bar's refresh button
   both live alongside native scroll on the same page. Which element is
   actually the page's scroll root (and so needs this to take effect on
   mobile Chrome/Safari) varies enough across browsers that it needs to
   be set on both html and body to reliably take hold everywhere.
   touch-action: pan-x pan-y (no pinch-zoom in that list) backs up the
   viewport meta tag's user-scalable=no for disabling pinch-zoom - more
   reliable across engines than the meta tag alone (iOS Safari in
   particular ignores user-scalable=no for pinch specifically). Doesn't
   conflict with elements that set their own narrower touch-action (the
   nav panel's pan-y, the edge-swipe zone, ...) - a descendant's
   touch-action can only narrow what an ancestor already allows, and pan-y
   is already inside pan-x pan-y. */
html {
  overscroll-behavior-y: contain;
  touch-action: pan-x pan-y;
}
body {
  margin: 0;
  font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
  background: var(--bg);
  color: var(--text);
  touch-action: pan-x pan-y;
  overscroll-behavior-y: contain;
}

a {
  color: var(--primary);
}

button, input, select {
  font: inherit;
}

.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.5rem 1rem;
  border-radius: var(--radius);
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  cursor: pointer;
}

/* .fasttap-touched: applied by core/fastTap.js right when it fires a
   fallback tap - :hover itself can't be triggered from JS (it tracks the
   browser's own live pointer position, not dispatched events), so a
   button/link the fallback just acted on would otherwise show no visual
   feedback at all despite working correctly. Kept in sync with .btn:hover
   itself, not a separate lookalike rule. */
.btn:hover,
.btn.fasttap-touched {
  border-color: var(--primary);
}

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

.btn-danger {
  border-color: var(--danger);
  color: var(--danger);
}

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1rem;
}

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

.field input, .field select, .field textarea {
  padding: 0.5rem;
  border-radius: var(--radius);
  border: 1px solid var(--border);
}
.field textarea {
  font: inherit;
  resize: vertical;
}

.error-banner {
  background: #fdecea;
  color: var(--danger);
  padding: 0.6rem 1rem;
  border-radius: var(--radius);
  margin-bottom: 1rem;
}

/* Shared across most views (was independently defined per-.vue-component
   under `<style scoped>`, so slight per-view variations below are folded
   into one canonical rule with a scoped override only where a real
   visual difference existed). */
.hint {
  color: var(--text-muted);
  font-size: 0.9rem;
}

.spacer {
  flex: 1;
}

.links {
  display: flex;
  justify-content: space-between;
  margin-top: 1rem;
  font-size: 0.85rem;
}

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

/* ---- App shell (was App.vue + DefaultLayout.vue) ---------------------- */
.layout {
  display: flex;
  min-height: 100vh;
}
.content {
  flex: 1;
  min-width: 0;
}

/* ---- Nav bar (was components/NavBar.vue) ------------------------------ */
.navbar {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 0.25rem;
  /* Fallback only - navBar.js sets this inline (from navWidth, or cleared
     to let .navbar.minimized's own fixed width apply) on every draw and
     continuously during a resize drag. Matters only for an instant before
     JS has run, or if it somehow hasn't. */
  width: 220px;
  flex-shrink: 0;
  padding: 1.25rem 1rem;
  background: var(--surface);
  border-right: 1px solid var(--border);
  color: var(--text);
  /* Pinned to the viewport while .content scrolls independently, instead
     of scrolling away with the page. Also what gives .nav-resize-handle
     below a containing block to position itself against. */
  position: sticky;
  top: 0;
  height: 100vh;
  overflow-y: auto;
  /* Without this, setting overflow-y alone makes the browser compute
     overflow-x as "auto" too (per spec) - and .nav-resize-handle sits 3px
     outside .navbar's own right edge on purpose (see below), which was
     just enough "overflow" to spawn a horizontal scrollbar. */
  overflow-x: hidden;
  /* Desktop-only in practice - the mobile media query below replaces this
     with its own transform/box-shadow transition instead, so this never
     actually animates width there. Explicitly turned off during a live
     resize drag (see navBar.js) so the width tracks the pointer directly
     instead of lagging behind it - this is what makes settling back on
     afterward (into the fixed minimized width, if collapsed) snap
     smoothly rather than just popping. */
  transition: width 0.2s ease;
}

/* Hidden by default (mobile) - an unstyled div would otherwise render
   in-flow as an empty block inside the off-canvas drawer's column
   layout, since nothing below the min-width query re-enables display
   there. Re-shown (as position:absolute, so back out of the flow again
   anyway) once desktop-width. */
.nav-resize-handle {
  display: none;
}

/* Desktop-only drag-to-resize handle and the minimized layout it can
   snap into (see the resize handling in navBar.js) - wrapped in this
   min-width query (rather than relying on navBar.js to know the
   viewport, or on after-the-fact overrides inside the existing
   max-width:768px block) specifically so it's impossible for the
   persisted, viewport-unaware navWidth/navMinimized to ever affect the
   mobile off-canvas panel: these rules simply don't exist below the
   breakpoint, at all, and stay correctly reactive to the window actually
   being resized across it (matching how every other desktop/mobile
   layout difference here is driven by media queries, not JS redraws). */
@media (min-width: 769px) {
  /* A thin invisible-until-hovered strip straddling .navbar's own right
     border, the actual drag target for resizing it. */
  .nav-resize-handle {
    display: block;
    position: absolute;
    top: 0;
    right: -3px;
    width: 6px;
    height: 100%;
    cursor: ew-resize;
    z-index: 10;
  }
  .nav-resize-handle:hover,
  .nav-resize-handle.dragging {
    background: var(--primary);
    opacity: 0.4;
  }

  /* Collapses the sidebar to just icons (module links, brand, avatar)
     plus whatever's already icon-only (settings, refresh) - the
     clock/weather block and every text label hide outright, there's no
     room/point for them at this width. */
  .navbar.minimized {
    width: 64px;
    padding-left: 0.5rem;
    padding-right: 0.5rem;
  }
  .navbar.minimized .nav-clock {
    display: none;
  }
  .navbar.minimized .nav-link-label,
  .navbar.minimized .nav-profile-name {
    display: none;
  }
  .navbar.minimized .nav-link,
  .navbar.minimized .brand,
  .navbar.minimized .nav-profile-link {
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .navbar.minimized .nav-avatar {
    margin-right: 0;
  }
  /* The footer row (avatar, settings, refresh) no longer fits side by
     side at this width once the profile name's gone - stack them
     instead. */
  .navbar.minimized .nav-footer {
    flex-direction: column;
    gap: 0.5rem;
  }
  .navbar.minimized .nav-footer .nav-profile-link {
    flex: none;
  }
}

/* Nav bar / mobile top bar theme (Settings -> Theme: system/light/dark) -
   deliberately scoped to just .navbar and .nav-toggle, not :root itself,
   so .content and every other view stays the plain light theme regardless
   (per request - only the nav chrome adapts for now). core/theme.js
   resolves the actual light/dark.js/dark-vs-light 'system' choice into a
   plain data-nav-theme="light"|"dark" attribute on <html> - this CSS never
   sees "system" itself.

   Overriding the shared tokens (--surface/--text/--border/...) here,
   rather than giving .navbar/.nav-toggle their own separately-named
   variables, means every child rule below (.brand, .nav-link,
   .nav-clock-*, .nav-footer, .nav-toggle-*, ...) that already reads
   var(--text)/var(--border)/etc picks up the right values automatically
   instead of duplicating each rule per theme.

   Dark is the default (:not([data-nav-theme="light"])) rather than gating
   behind [data-nav-theme="dark"] specifically - matches this app's
   original always-dark sidebar look, so there's no flash of the wrong
   theme for the instant before core/theme.js's JS has run. */
:root:not([data-nav-theme='light']) .navbar,
:root:not([data-nav-theme='light']) .nav-toggle {
  --surface: #1c1c27;
  --surface-muted: #2a2a38;
  --text: #e8e8f0;
  --text-muted: #9a9aad;
  --border: rgba(255, 255, 255, 0.1);
  --primary: #818cf8;
}
:root[data-nav-theme='light'] .navbar,
:root[data-nav-theme='light'] .nav-toggle {
  --surface: #ffffff;
  --surface-muted: #f0f1f5;
  --text: #1c1c24;
  --text-muted: #6b6b7a;
  --border: #e2e2ea;
  --primary: #4f46e5;
}

/* Compact mobile top bar (hamburger toggle) - hidden on desktop, where
   the sidebar above is always visible. */
.nav-toggle {
  display: none;
}
.nav-backdrop {
  display: none;
}
.nav-edge-zone {
  display: none;
}

@media (max-width: 768px) {
  /* .navbar itself goes position:fixed below (off-canvas), so the only
     thing left in normal flow inside .layout's row is .nav-toggle - as a
     flex item with no explicit width it would otherwise just shrink-wrap
     to its own content ("🏠 FamilyName ☰"), leaving .content to fill the
     rest of that same row instead of sitting full-width underneath it.
     Stacking .layout vertically makes both stretch full-width instead
     (flex's default align-items: stretch on the cross axis). */
  .layout {
    flex-direction: column;
  }

  /* .nav-toggle's own parent has no real height (see note above - .navbar
     next to it is position:fixed, contributing nothing to the flow), so
     position:sticky had nowhere to "hold" it: once scrolled past that
     sliver of a parent, it scrolled away despite `top: 0`. position:fixed
     pins it to the viewport unconditionally instead - .content gets
     matching top padding below so its content doesn't render underneath. */
  :root {
    --mobile-topbar-height: 52px;
  }
  .nav-toggle {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    min-height: var(--mobile-topbar-height);
    box-sizing: border-box;
    z-index: 60;
    padding: 0.6rem 1rem 0.6rem 0.25rem;
    /* Was hardcoded to the same hex values .navbar used to hardcode too -
       now themed the same way (see the data-nav-theme rules above). */
    background: var(--surface);
    color: var(--text);
    border-bottom: 1px solid var(--border);
  }
  .nav-toggle-title {
    font-weight: 700;
  }
  .nav-toggle-btn, .nav-toggle-action-btn {
    background: none;
    border: none;
    color: inherit;
    font-size: 1.4rem;
    line-height: 1;
    padding: 0.25rem 0.5rem;
    cursor: pointer;
  }
  /* Per-view buttons a module contributes (see core/topBarActions.js) -
     slightly smaller than the ☰ toggle, same idea .nav-toggle-refresh-btn
     used to be sized at before that button moved into .nav-footer. */
  .nav-toggle-action-btn {
    font-size: 1.1rem;
  }
  .content {
    padding-top: var(--mobile-topbar-height);
  }

  /* The mobile top bar already shows the current page/module name (see
     navBar.js currentPageLabel()) - each page's own <h1> would just
     repeat it. Scoped to .content (the authenticated shell's outlet) so
     guest pages (login, setup, ...), which don't have this top bar at
     all, keep their own <h1>. */
  .content h1 {
    display: none;
  }

  /* The sidebar becomes an off-canvas panel, slid in over the content
     rather than pushing it, and toggled by .nav-toggle-btn above. */
  .navbar {
    position: fixed;
    top: 0;
    left: 0;
    width: min(280px, 85vw);
    z-index: 80;
    transform: translateX(-100%);
    transition: transform 0.2s ease, box-shadow 0.2s ease;
    /* No box-shadow here on the closed (base) state - box-shadow isn't
       clipped to the element's own box, so even fully translated off
       -screen, its blur radius (16px, plus the 2px x-offset = up to 18px)
       was still bleeding into the visible edge of the screen. Only
       showing it once actually .open avoids that entirely - transitioned
       alongside transform above so it still fades in/out smoothly rather
       than popping. */
    touch-action: pan-y;
  }
  .navbar.open {
    transform: translateX(0);
    box-shadow: 2px 0 16px rgba(0, 0, 0, 0.3);
  }

  .nav-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.4);
    z-index: 70;
  }
  .nav-backdrop.open {
    display: block;
  }

  /* Real hit-target for the edge-swipe-to-open gesture (see navBar.js).
     Deliberately invisible - width comes from JS (inline style, kept in
     sync with the drag-detection threshold there). touch-action: pan-y is
     the actual point of this element: Chrome for Android reserves roughly
     this same outer strip for its own "swipe from edge to go back"
     gesture and can swallow the touch before page JS ever runs at all -
     but that arbitration looks at the touch-action of the element the
     gesture *starts* on, so setting it here (as opposed to e.g. relying
     on a document-level pointerdown listener with a coordinate check,
     which has no element to hang the CSS off of) is what actually gives
     the page's own drag handler a chance to run instead.
     z-index sits below .nav-toggle (60) so the top bar - including the ☰
     button, which lives flush against this same edge - still wins there
     and gets its own tap handling undisturbed; below .nav-backdrop (70)
     and the open .navbar (80) too, so this zone never intercepts taps on
     the panel itself once it's open (moot anyway once open, since
     onEdgePointerDown bails out while menuOpen is true - but keeps the
     stacking order correct regardless). Above ordinary page content
     (z-index: auto) so it's actually reachable there. */
  .nav-edge-zone {
    display: block;
    position: fixed;
    top: var(--mobile-topbar-height);
    left: 0;
    bottom: 0;
    z-index: 55;
    touch-action: pan-y;
  }
}

.nav-clock {
  margin-bottom: 1rem;
  padding-bottom: 0.75rem;
  border-bottom: 1px solid var(--border);
  cursor: pointer;
}
.nav-clock-date {
  font-size: 0.8rem;
  color: var(--text-muted);
}
.nav-clock-row {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
}
.nav-clock-time {
  font-size: 2.25rem;
  font-weight: 600;
}
.nav-weather {
  text-align: right;
}
.nav-weather-location {
  font-size: 0.75rem;
  color: var(--text-muted);
}
.nav-weather-temp {
  font-size: 0.95rem;
  color: var(--text-muted);
}
.brand {
  display: block;
  font-weight: 700;
  margin-bottom: 0.75rem;
  /* Now an <a> (links to the dashboard) - keep it looking like a plain
     bold header rather than a normal colored/underlined link. */
  color: inherit;
  text-decoration: none;
}
.nav-link {
  color: var(--text);
  text-decoration: none;
  padding: 0.5rem 0.75rem;
  border-radius: var(--radius);
}
.nav-link.active {
  background: var(--surface-muted);
  color: var(--primary);
}

/* Last row of the panel: profile link on the left, settings gear pinned to
   the bottom-right corner next to it. */
.nav-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.25rem;
}
.nav-footer .nav-profile-link {
  flex: 1;
  min-width: 0;
}
/* Styled like .brand (plain flush-left label, no padding/pill-highlight),
   not like .nav-link - it's the one link in the panel that isn't really
   "another nav item" so much as an identity label. */
.nav-profile-link {
  display: flex;
  align-items: center;
  min-width: 0;
  color: inherit;
  text-decoration: none;
}
.nav-profile-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Placeholder avatar circle - shows the user's initials (see navBar.js
   initials()), or the generic person-silhouette SVG when there's no name
   yet to derive them from. No real avatar upload/photo yet either way. */
.nav-avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 50%;
  background: var(--surface-muted);
  border: 1px solid var(--text-muted);
  color: var(--text-muted);
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  /* Without this, the initials text's vertical position inside the
     circle depends on the active font's own line-height metrics, which
     differ enough between platforms (desktop's Segoe UI vs. Android's
     Roboto vs. iOS's San Francisco - see the font stack on body) to
     visibly shift where flexbox centers it. Pinning line-height removes
     that per-font variance so the centering is identical everywhere. */
  line-height: 1;
  overflow: hidden;
  margin-right: 0.5rem;
}
/* Shared "icon-only button in .nav-footer" look - settingsLink() and the
   refresh button in navBar.js. */
.nav-settings-link, .nav-refresh-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: var(--radius);
  font-size: 1.1rem;
  text-decoration: none;
  color: var(--text-muted);
  /* .nav-refresh-btn is a <button>, not an <a> like .nav-settings-link -
     these reset it back to looking the same as the link (font-size
     above already covers what matters for a single emoji icon, so no
     font-family/line-height reset needed on top of that). */
  background: none;
  border: none;
  cursor: pointer;
}
.nav-settings-link.active {
  background: var(--surface-muted);
  color: var(--primary);
}

/* ---- Auth pages (login/setup/forgot/reset/accept-invite) -------------- */
.auth-page {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  padding: 1rem;
}
.auth-card {
  width: 100%;
  max-width: 360px;
}
.auth-card.wide {
  max-width: 400px;
}
.not-found {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 60vh;
  gap: 0.5rem;
}

/* ---- Dashboard (was views/DashboardView.vue) --------------------------- */
.dashboard-page {
  max-width: 1100px;
  margin: 0 auto;
  padding: 1rem;
}
.tabs {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 1rem;
  flex-wrap: wrap;
}
.tabs .btn.active {
  border-color: var(--primary);
  color: var(--primary);
}

/* ---- Toolbar (row of icon buttons under a page title) ------------------ */
.toolbar {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.add-widget {
  margin: 1rem;
}
.add-widget-row {
  display: flex;
  gap: 0.75rem;
  flex-wrap: wrap;
}
.add-widget .hint {
  font-size: 0.85rem;
  margin-top: 0.5rem;
}

/* ---- Widget grid (was components/WidgetGrid.vue) ----------------------- */
.grid-wrap {
  padding: 1rem;
}
.grid {
  position: relative;
  min-height: 120px;
}
.cell {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
.cell.editable {
  border-style: dashed;
}
.cell.dragging {
  z-index: 10;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
.cell-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.25rem 0.4rem;
  background: var(--surface-muted);
  cursor: grab;
  font-size: 0.85rem;
}
.drag-handle {
  cursor: grab;
}
.remove-btn {
  border: none;
  background: none;
  cursor: pointer;
  color: var(--danger);
}
.cell-body {
  flex: 1;
  min-height: 0;
  overflow: auto;
}
.resize-handle {
  position: absolute;
  right: 0;
  bottom: 0;
  width: 14px;
  height: 14px;
  cursor: nwse-resize;
  background: linear-gradient(135deg, transparent 50%, var(--border) 50%);
}

@media (max-width: 640px) {
  /* The 12-column layout is set via inline styles (widgetGrid.js), sized
     for pointer-drag editing - not workable on a phone. Stack widgets full
     width instead; `!important` is needed to beat those inline styles.
     `.cell`'s own grid-column/row become inert once the parent isn't a
     grid, so only the parent needs overriding. */
  .grid {
    display: block !important;
  }
  .cell {
    margin-bottom: 12px;
  }
  .cell-header {
    cursor: default;
  }
  .drag-handle,
  .resize-handle {
    display: none;
  }
}
.missing {
  padding: 0.75rem;
  font-size: 0.8rem;
  color: var(--text-muted);
}
.empty-hint {
  grid-column: 1 / -1;
  color: var(--text-muted);
  text-align: center;
  padding: 2rem;
}

/* ---- Profile (was views/ProfileView.vue) -------------------------------- */
.profile-page {
  max-width: 480px;
  margin: 2rem auto;
  padding: 0 1rem;
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}

/* Moved here from the nav bar - one Log out control instead of two, and it
   now lives alongside the rest of the account-level actions (password
   change, etc.) instead of floating at the end of the nav links. */
.profile-logout {
  display: flex;
  justify-content: center;
}

/* ---- Settings (was views/SettingsView.vue) ------------------------------ */
.settings-page {
  max-width: 560px;
  margin: 2rem auto;
  padding: 0 1rem;
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}
.module-list {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}
.module-row {
  display: flex;
  align-items: flex-start;
  gap: 1rem;
  flex-wrap: wrap;
  border-bottom: 1px solid var(--border);
  padding-bottom: 1rem;
}
.module-info {
  flex: 1;
  min-width: 0;
}
.module-order {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}
.module-order .btn {
  padding: 0.15rem 0.5rem;
  line-height: 1.2;
}
.module-order .btn:disabled {
  opacity: 0.35;
  cursor: default;
}
.module-row:last-child {
  border-bottom: none;
  padding-bottom: 0;
}
.badge {
  font-size: 0.7rem;
  background: var(--surface-muted);
  border-radius: 999px;
  padding: 0.1rem 0.5rem;
  margin-left: 0.4rem;
}
.toggle {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  white-space: nowrap;
}

/* ---- Members/invites (was the standalone /users route, now folded into
   the Settings page above - see SettingsView.js) --------------------------- */
/* Lets a wide table scroll horizontally within itself on narrow screens
   instead of overflowing/breaking the page layout. */
.table-scroll {
  overflow-x: auto;
}
.table {
  width: 100%;
  border-collapse: collapse;
}
.table th, .table td {
  text-align: left;
  padding: 0.5rem;
  border-bottom: 1px solid var(--border);
}
.invite-form {
  display: flex;
  align-items: flex-end;
  gap: 1rem;
}
.invite-url {
  font-size: 0.8rem;
  word-break: break-all;
}

/* ---- Modal (components/modal.js) --------------------------------------- */
.modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  z-index: 100;
}
.modal-card {
  width: 100%;
  max-width: 420px;
  max-height: 90vh;
  overflow-y: auto;
}
.modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 1rem;
}
.modal-header h2 {
  margin: 0;
  font-size: 1.1rem;
}
.modal-close {
  border: none;
  background: none;
  cursor: pointer;
  font-size: 1.1rem;
  line-height: 1;
  padding: 0.25rem;
  color: var(--text-muted);
}
.modal-close:hover {
  color: var(--text);
}

/* ---- Screensaver (components/screensaver.js) ---------------------------- */
/* Just the black background - stays fully opaque during a dismiss-swipe
   drag (see .screensaver-content below, which is what actually fades/
   slides during the drag itself) and only fades on its own once the
   swipe commits (via .active being removed) or on the initial fade-in. */
.screensaver {
  position: fixed;
  inset: 0;
  z-index: 1000; /* above everything else, including the mobile nav panel (80) and its backdrop (70) */
  background: #000;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.6s ease;
  /* Without this, a touch starting here still counts (via the ancestor-
     intersection rule touch-action uses) as allowing native panning -
     inherited from html/body's touch-action: pan-x pan-y (added for the
     pinch-zoom-disable feature) - so a dismiss-swipe could scroll the
     page *underneath* the overlay at the same time as the JS-driven
     fade/slide above, showing its scrollbar. none claims the touch
     entirely: no native scroll/zoom, only the pointermove handlers below
     get to react to it. Applies to everything inside too (including
     .screensaver-content and the clock) - a descendant's touch-action can
     only narrow an ancestor's, never widen it back past `none`. */
  touch-action: none;
}
.screensaver.active {
  opacity: 1;
  pointer-events: auto;
}
/* Everything visible (date/clock/weather/...) - see screensaver.js's swipe
   handlers, which fade this as a whole (not .screensaver itself) live as
   the drag progresses, while each individual piece inside (.screensaver-
   piece below) flies outward on its own. Fills the same full-screen area
   .screensaver used to size its flex layout against, so the corner-pinned
   elements inside (.screensaver-date, .screensaver-hebrew-date,
   .screensaver-sun-times) still position the same as before, just
   relative to this instead. */
.screensaver-content {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}
/* Applied (alongside each piece's own class) to every direct "explosion
   piece" - see explosionPieces in screensaver.js. transform is what
   animates a piece flying outward on a dismiss-swipe and springing back
   if the swipe doesn't commit; opacity isn't actually used per-piece
   (that's still one shared fade on .screensaver-content), kept here
   anyway so a piece transitions consistently if that ever changes. */
.screensaver-piece {
  transition: opacity 0.4s ease, transform 0.4s ease;
}
/* Gregorian date, top-left corner - independent of the centered clock/
   weather stack, so it sits fixed to the corner regardless of how tall
   that stack is on any given screen. Mirrors the Jewish date's top-right
   treatment below. */
.screensaver-date {
  position: absolute;
  top: clamp(1rem, 4vw, 3rem);
  left: clamp(1rem, 4vw, 3rem);
  text-align: center;
  user-select: none;
}
.screensaver-date-weekday {
  font-size: clamp(1.1rem, 2.8vw, 1.8rem);
  font-weight: 300;
  color: #888;
}
.screensaver-date-day {
  font-size: clamp(2.4rem, 5.8vw, 4.2rem);
  font-weight: 400;
  color: #ccc;
  line-height: 1.3;
}
.screensaver-date-month {
  font-size: clamp(1.1rem, 2.8vw, 1.8rem);
  font-weight: 300;
  color: #888;
}
/* Sits above the clock, centered - part of the same centered flex column
   as .screensaver-clocks/.screensaver-weather (unlike the date/weather/
   sun-times corner-and-bottom elements, which are position: absolute). */
.screensaver-moon-phase {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.4rem;
  margin-bottom: 0.5rem;
}
/* NASA's real rendered-moon image (see getMoonImageUrl in core/clock.js) -
   hidden by default until updateMoonPhase() finds a URL for the current
   hour; also hidden again on a load error (see the img's error listener
   in screensaver.js) rather than showing a broken-image icon. */
.screensaver-moon-image {
  width: clamp(4rem, 11vw, 8rem);
  height: clamp(4rem, 11vw, 8rem);
  border-radius: 50%;
  user-select: none;
  -webkit-user-drag: none;
}
.screensaver-moon-label {
  /* Hidden for now - the same text lives as the image's title="" tooltip
     instead (see updateMoonPhase in screensaver.js). Still kept updated,
     so bringing this back is just deleting this one rule. */
  display: none;
  font-size: clamp(1.1rem, 2.8vw, 1.8rem);
  font-weight: 300;
  color: #888;
  user-select: none;
}
.screensaver-time {
  /* Scales with the viewport ("size adjusted to screen size") rather than
     a fixed rem value, clamped so it stays reasonable on very small or
     very large/ultra-wide screens. */
  font-size: clamp(3rem, 20vw, 16rem);
  font-weight: 300;
  line-height: 1;
  color: #ccc;
  font-variant-numeric: tabular-nums; /* keeps digit widths steady as the minute changes, instead of the whole clock jittering left/right */
  user-select: none;
}
.screensaver-weather {
  font-size: clamp(1rem, 3vw, 2rem);
  font-weight: 300;
  color: #888; /* same dim tone as the date - both secondary to the time */
  margin-top: 0.5rem;
  user-select: none;
}

/* Jewish date, top-right corner - independent of the centered date/clock/
   weather stack, so it sits fixed to the corner regardless of how tall
   that stack is on any given screen. */
.screensaver-hebrew-date {
  position: absolute;
  top: clamp(1rem, 4vw, 3rem);
  right: clamp(1rem, 4vw, 3rem);
  text-align: center;
  user-select: none;
}
.screensaver-hebrew-weekday {
  font-size: clamp(1.1rem, 2.8vw, 1.8rem);
  font-weight: 300;
  color: #888;
}
.screensaver-hebrew-day {
  font-size: clamp(2.4rem, 5.8vw, 4.2rem);
  font-weight: 400;
  color: #ccc;
  line-height: 1.3;
}
.screensaver-hebrew-month {
  font-size: clamp(1.1rem, 2.8vw, 1.8rem);
  font-weight: 300;
  color: #888;
}

/* Sunrise/sunset, bottom of the screen - independent of the centered
   date/clock/weather stack for the same reason the Hebrew date is: stays
   pinned in place regardless of how tall that stack is. */
.screensaver-sun-times {
  position: absolute;
  bottom: clamp(1rem, 4vw, 2rem);
  /* Centered via left:0/right:0/auto-margins rather than the more common
     left:50%+transform:translateX(-50%) trick - this element is also one
     of screensaver.js's explosion pieces, which sets its own transform
     (of a totally different kind - see EXPLODE_DISTANCE_MULTIPLIER) via
     inline style during a dismiss-swipe. Inline style fully replaces
     transform rather than composing with it, so translateX(-50%) here
     would just vanish the instant a drag starts, snapping this element
     sideways. Avoiding transform for the base centering sidesteps that
     entirely instead of needing the JS to know how to preserve/reconstruct
     a piece's own base transform. */
  left: 0;
  right: 0;
  width: fit-content;
  margin: 0 auto;
  font-size: clamp(0.75rem, 1.6vw, 1rem);
  font-weight: 300;
  color: #666;
  white-space: nowrap;
  user-select: none;
}

/* Digital/analog picker (long-press the clock) - both clock faces sit on
   top of each other, centered, absolutely positioned so switching which
   one is showing (or splitting them apart into the picker) only ever
   animates position/scale/opacity, never layout - nothing else on the
   screensaver has to shift around for it. */
.screensaver-clocks {
  position: relative;
  width: 100%;
  min-height: clamp(10rem, 34vw, 24rem);
}
.screensaver-clock {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%) scale(0.5);
  opacity: 0;
  pointer-events: none;
  /* Border is always present (transparent when not in picker mode) so it
     has a fixed size to transition to/from, instead of a sudden 0 -> 2px
     jump in the box's dimensions - which translate(-50%, -50%) centers
     around - the instant picker mode toggles on. */
  border: 2px solid transparent;
  border-radius: 16px;
  padding: 1rem 1.5rem;
  transition: left 0.6s ease, transform 0.6s ease, opacity 0.6s ease, border-color 0.6s ease, background-color 0.6s ease;
  cursor: pointer;
}
/* Scoped under .screensaver.active so a clock can never end up
   click-through-able while the overlay itself is hidden - pointer-events
   is inheritable, so a bare .screensaver-clock.active rule would silently
   re-enable it even behind an inactive (pointer-events: none) overlay. */
.screensaver.active .screensaver-clock.active {
  opacity: 1;
  pointer-events: auto;
  transform: translate(-50%, -50%) scale(1);
}
.screensaver.active .screensaver-clocks.picker .screensaver-clock {
  opacity: 1;
  pointer-events: auto;
  transform: translate(-50%, -50%) scale(0.55);
  /* The frame that makes each one read as a tappable button while
     choosing between them - not shown outside picker mode, where there's
     only ever one clock visible and nothing to choose between. */
  border-color: #666;
  background-color: rgba(255, 255, 255, 0.05);
}
.screensaver.active .screensaver-clocks.picker .screensaver-clock:hover {
  border-color: #ccc;
}
.screensaver-clocks.picker .screensaver-clock[data-type='digital'] {
  left: 30%;
}
.screensaver-clocks.picker .screensaver-clock[data-type='analog'] {
  left: 70%;
}

.screensaver-analog-face {
  width: clamp(10rem, 32vw, 22rem);
  height: clamp(10rem, 32vw, 22rem);
  display: block;
}
.screensaver-analog-rim {
  fill: none;
  stroke: #ccc;
  stroke-width: 2;
}
.screensaver-analog-face line {
  stroke: #ccc;
  stroke-linecap: round;
}
.screensaver-analog-tick {
  stroke-width: 1.5;
}
.screensaver-analog-hour {
  stroke-width: 4;
}
.screensaver-analog-minute {
  stroke-width: 2.5;
}
.screensaver-analog-pin {
  fill: #ccc;
}
