@font-face {
  font-family: "Accuratist";
  src: url("fonts/Accuratist.woff2") format("woff2");
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}

:root {
  --sr-black: #000000;
  --sr-near-black: #0e0e10;
  --sr-dark-gray: #1c1c1f;
  --sr-mid-gray: #3a3a3f;
  --sr-light-gray: #b8b8c0;
  --sr-white: #ffffff;
  --sr-purple: #ff5c0f;
  --sr-purple-dark: #cc4a0c;
  --sr-purple-light: #ff9d6f;

  /* Overridden at runtime from settings.schedule_now_line_color (see
     app.js's applyNowLineColor) -- the -rgb variant exists because the
     glow needs an alpha-blended box-shadow, which rgba() can't build
     from a hex custom property directly. */
  --sr-now-line: #b91c1c;
  --sr-now-line-rgb: 185, 28, 28;

  /* Per-lesson attendance markers (see app.js's attendanceIcon) -- drawn
     on a light-orange (or, for "not required", gray) badge, so these need
     to be dark/saturated enough to read clearly on a light background
     rather than the near-black one everywhere else in the app. */
  --sr-status-badge-bg: #fca580;
  --sr-status-present: #16a34a;
  --sr-status-absent: #dc2626;
  --sr-status-not-required-bg: #9a9ca3;
}

* { box-sizing: border-box; }

/* Browsers don't reliably inherit body's font-family into form controls
   (button/input/select/textarea default to a platform UI font instead) --
   confirmed live: nav-item buttons were rendering in the system font while
   surrounding text used Accuratist. Force it explicitly everywhere. */
button, input, select, textarea { font-family: inherit; }

/* Accuratist renders visibly smaller than the system-font stack it
   replaced at the same declared size -- bump the root size so every
   `rem`-based font-size in this file (all of them) scales up together
   instead of editing each one individually. */
html { font-size: 19px; }

body {
  margin: 0;
  background: var(--sr-near-black);
  color: var(--sr-white);
  font-family: "Accuratist", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  min-height: 100vh;
  position: relative;
}

/* Brand watermark: the logo mark, tinted a gray a touch lighter than the
   page background, pinned to the bottom-right corner behind all content.
   The source PNG is solid white on transparent, so mask-image (rather than
   background-image) is what lets it be recolored to any tint via
   background-color -- the mask just reuses the logo's own alpha shape.
   z-index:-1 on a fixed/positioned element paints it below the page's
   normal in-flow content (see CSS stacking rules) but above body's own
   flat background, so it shows through empty space and gets naturally
   covered by real UI wherever content actually sits on top of it. */
body::before {
  content: "";
  position: fixed;
  bottom: 0;
  right: 0;
  width: 70vw;
  height: 70vh;
  background-color: var(--sr-dark-gray);
  -webkit-mask-image: url("logo.png");
  mask-image: url("logo.png");
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: bottom right;
  mask-position: bottom right;
  -webkit-mask-size: contain;
  mask-size: contain;
  pointer-events: none;
  z-index: -1;
}

.hidden { display: none !important; }

/* ---- Login screen ---- */
#login-screen {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}

.login-card {
  width: 100%;
  max-width: 360px;
  text-align: center;
}

.login-card h1 {
  font-size: 1.6rem;
  margin: 0 0 24px;
}

input[type="password"], input[type="text"] {
  width: 100%;
  padding: 12px 14px;
  border-radius: 10px;
  border: 1px solid var(--sr-mid-gray);
  background: var(--sr-dark-gray);
  color: var(--sr-white);
  font-size: 1rem;
  margin-bottom: 12px;
}

input:focus { outline: 2px solid var(--sr-purple); border-color: transparent; }

button {
  width: 100%;
  padding: 12px 14px;
  border-radius: 10px;
  border: none;
  background: var(--sr-purple);
  color: var(--sr-white);
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
}

button:hover { background: var(--sr-purple-dark); }
button:disabled { background: var(--sr-mid-gray); cursor: default; }

button.secondary {
  background: transparent;
  border: 1px solid var(--sr-mid-gray);
  color: var(--sr-light-gray);
}

.error-text { color: #f87171; font-size: 0.9rem; margin: 8px 0; min-height: 1.2em; }
.success-text { color: #4ade80; font-size: 0.9rem; margin: 8px 0; min-height: 1.2em; }

/* ---- App shell ---- */
#app-shell {
  display: flex;
  /* Fixed, not min-height -- min-height let #app-shell (and the whole
     page) grow taller than the viewport with long content, which made
     main.content's own overflow-y:auto below inert (it was never actually
     shorter than its content, so it never had anything to scroll) and the
     native page scroll move the sidebar right along with everything else.
     Pinning this to exactly the viewport height is what makes "only the
     content area scrolls, the sidebar stays put" actually happen. */
  height: 100vh;
}

/* Fixed overlay rather than a flex child of #app-shell -- avoids
   disturbing the sidebar/content row layout just to show a transient
   "you're offline" message (see api()'s offline fallback in app.js). */
#offline-banner {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 50;
  background: #7a1f1f;
  color: #ffb4b4;
  text-align: center;
  font-size: 0.85rem;
  font-weight: 600;
  padding: 8px 12px;
}

/* Fixed for the same reason as #offline-banner above -- shown on every
   page (see app.js's initGlobalClock) regardless of which view/section is
   active, so it can't be a normal flow child of any one of them. Sits
   below #offline-banner's z-index so a rare simultaneous "you're offline"
   banner still wins the top-right corner. */
#global-clock {
  position: fixed;
  top: 33px;
  right: 20px;
  z-index: 40;
  background: var(--sr-dark-gray);
  color: var(--sr-light-gray);
  font-size: 0.8rem;
  padding: 6px 14px;
  border-radius: 999px;
  white-space: nowrap;
}

nav.sidebar {
  width: 220px;
  flex-shrink: 0;
  background: var(--sr-black);
  padding: 20px 12px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

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

nav.sidebar .brand img { width: 32px; height: 32px; }
nav.sidebar .brand span { font-weight: 700; font-size: 1.1rem; }

nav.sidebar button.nav-item {
  background: transparent;
  color: var(--sr-light-gray);
  text-align: left;
  font-weight: 500;
  padding: 10px 12px;
}

nav.sidebar button.nav-item.active {
  background: var(--sr-dark-gray);
  color: var(--sr-white);
  border-left: 3px solid var(--sr-purple);
}

nav.sidebar button.nav-item:hover { background: var(--sr-dark-gray); }

main.content {
  flex: 1;
  padding: 28px 32px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
}

main.content h2 {
  margin-top: 0;
  border-bottom: 1px solid var(--sr-mid-gray);
  padding-bottom: 12px;
}

/* ---- Settings ---- */
main.content h3 { font-size: 1rem; margin: 20px 0 8px; color: var(--sr-light-gray); }
main.content h3:first-of-type { margin-top: 0; }
.settings-card {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 4px;
}
.settings-card input[type="text"],
.settings-card input[type="password"] {
  width: 100%;
  max-width: 360px;
}
.settings-card input[type="number"] { padding: 8px 10px; margin-bottom: 8px; }
.settings-card input[type="color"] {
  width: 48px;
  height: 36px;
  padding: 2px;
  border: 1px solid var(--sr-mid-gray);
  border-radius: 8px;
  background: var(--sr-near-black);
  cursor: pointer;
}
.settings-card label.meta { margin-top: 6px; }
.settings-card button.btn-small { margin-top: 8px; }
.settings-time-row { display: flex; align-items: center; gap: 6px; }
.settings-toggle-row { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; cursor: pointer; }
.settings-keybind-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: 10px 16px;
  width: 100%;
  margin: 8px 0;
}
.settings-keybind-grid label { display: flex; align-items: center; justify-content: space-between; gap: 8px; color: var(--sr-light-gray); }
.settings-keybind-grid input[type="text"] {
  width: 40px !important;
  max-width: 40px !important;
  text-align: center;
  text-transform: uppercase;
}
.settings-toggle-row input[type="checkbox"] { width: auto; margin: 0; }

.settings-status-details { margin: 20px 0 12px; }
.settings-status-summary {
  cursor: pointer;
  font-size: 1rem;
  color: var(--sr-light-gray);
}
.settings-status-table td { vertical-align: top; }
.settings-status-table td:first-child { white-space: nowrap; }
.settings-status-pending { color: var(--sr-light-gray); font-size: 0.85rem; }

.card {
  background: var(--sr-dark-gray);
  border-radius: 12px;
  padding: 16px 18px;
  margin-bottom: 12px;
}

.card .meta { color: var(--sr-light-gray); font-size: 0.85rem; }

/* Studie+'s injected news HTML (see modules/forside.py's news_html) is raw
   exported markup we don't control the shape of -- its own <p>/<div> tags
   carry the browser's default top margin, which stacks on top of .card's
   own padding-top and makes the gap above the first line much bigger than
   the side padding. Zeroing every descendant's top margin removes that
   without touching bottom margins, so spacing between paragraphs still
   reads normally -- just nothing pushes the first line down from the top. */
.news-card { padding-top: 18px; padding-bottom: 18px; }
.news-card #news-filtered-content *,
.news-card #news-original-content * { margin-top: 0; }

table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.9rem;
}

th, td {
  text-align: left;
  padding: 8px 10px;
  border-bottom: 1px solid var(--sr-mid-gray);
}

th { color: var(--sr-light-gray); font-weight: 600; }

th.sortable { cursor: pointer; user-select: none; }
th.sortable:hover { color: var(--sr-white); }

.date-cell { cursor: pointer; text-decoration: underline dotted var(--sr-mid-gray) 1px; text-underline-offset: 3px; }

#assignments-list tr[data-id]:hover { background: var(--sr-mid-gray); }

.badge {
  display: inline-block;
  background: var(--sr-purple);
  color: var(--sr-white);
  border-radius: 999px;
  padding: 2px 10px;
  font-size: 0.75rem;
  font-weight: 600;
}
.badge--failed { background: var(--sr-status-absent); }

.status-line { font-size: 1.1rem; margin-bottom: 6px; }
.status-line .accent { color: var(--sr-purple-light); font-weight: 600; }

/* ---- Sync status (Home tab) -- see refresh.get_module_status ---- */
.sync-stale-badge {
  display: inline-block;
  background: #7a1f1f;
  color: #ffb4b4;
  border-radius: 999px;
  padding: 2px 10px;
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.02em;
}
/* ---- Absence budget badge (Schedule tab week toolbar) ---- */
.budget-badge {
  display: inline-block;
  border-radius: 999px;
  padding: 2px 10px;
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.02em;
}
.budget-badge--ok { background: #14532d; color: #86efac; }
.budget-badge--over { background: #7a1f1f; color: #ffb4b4; }

/* Neutral purple, not the red .sync-stale-badge uses -- this isn't a
   problem, just a status ("people_photos in particular can run for tens
   of minutes", see refresh.get_module_status). */
.sync-in-progress-badge {
  display: inline-block;
  background: var(--sr-mid-gray);
  color: var(--sr-purple-light);
  border-radius: 999px;
  padding: 2px 10px;
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.02em;
}
.sync-now-btn { white-space: nowrap; }
.sync-now-btn:disabled { opacity: 0.6; cursor: default; }
/* No per-module percentage exists server-side (see refresh.get_module_status
   -- in_progress is a plain bool, no step/byte count to derive one from),
   so this is an indeterminate sliding bar rather than a filling one --
   just enough to read as "actively working", not a real completion measure. */
.sync-progress-row td { padding-top: 0; padding-bottom: 8px; }
.sync-progress-bar {
  height: 4px;
  border-radius: 999px;
  background: var(--sr-mid-gray);
  overflow: hidden;
}
.sync-progress-bar-fill {
  height: 100%;
  width: 35%;
  border-radius: 999px;
  background: var(--sr-purple);
  animation: sync-progress-slide 1.1s ease-in-out infinite;
}
@keyframes sync-progress-slide {
  0% { margin-left: -35%; }
  100% { margin-left: 100%; }
}
/* Locks the button to the far right edge of the card regardless of how
   wide the other columns end up (module name length, "stale" badge
   presence, etc.) -- text-align:left on the cell (the table default)
   would instead let the button sit whichever amount left of the edge its
   own column happens to be, not visibly consistent row to row. */
.sync-now-cell { text-align: right; width: 1%; white-space: nowrap; }
.sync-now-cell .sync-stale-badge { margin-right: 8px; }

/* ---- Home "What's new" (Forside) warning -- same palette as
   #offline-banner/.sync-stale-badge, but in-flow (not fixed/pill) since
   this is real page content, not a transient overlay. */
.home-warning-banner {
  background: #7a1f1f;
  color: #ffb4b4;
  border-radius: 8px;
  padding: 12px 16px;
  font-weight: 600;
  margin: 12px 0;
}

/* ---- Toolbars, filters, week nav ---- */
.view-header-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  border-bottom: 1px solid var(--sr-mid-gray);
  padding-bottom: 12px;
}
.view-header-row h2 { border-bottom: none; padding-bottom: 0; margin: 0; }
.view-header-row button#compose-button { width: auto; padding: 8px 16px; }
/* Reserves room for #global-clock (fixed, top-right, see above) so its
   widest realistic text ("8th of August · Week 32 · 18:21:31") never
   sits under the compose button -- specific to this row (the only place
   in the app with a right-aligned button up near the top), not a
   content-wide padding change on every page. */
.view-header-row { padding-right: 250px; }

.week-nav {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 16px;
}

/* Day-at-a-time nav for the schedule -- desktop keeps showing the whole
   week (see the schedule-grid rules below), so this stays hidden until the
   720px breakpoint switches the grid over to a single day. */
.day-nav {
  display: none;
  align-items: center;
  gap: 12px;
  margin-bottom: 16px;
}

.btn-small {
  width: auto;
  padding: 6px 12px;
  font-size: 0.85rem;
  background: var(--sr-dark-gray);
  border: 1px solid var(--sr-mid-gray);
}
.btn-small:hover { background: var(--sr-mid-gray); }
.btn-danger-armed {
  background: var(--sr-status-absent);
  border-color: var(--sr-status-absent);
  color: var(--sr-white);
}
.btn-danger-armed:hover { background: var(--sr-status-absent); }

.toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: center;
  margin-bottom: 16px;
}
.toolbar input[type="text"] { max-width: 320px; margin-bottom: 0; }

.filter-group { display: flex; gap: 6px; flex-wrap: wrap; }
.btn-filter {
  width: auto;
  padding: 6px 14px;
  font-size: 0.85rem;
  background: var(--sr-dark-gray);
  border: 1px solid var(--sr-mid-gray);
  color: var(--sr-light-gray);
}
.btn-filter.active { background: var(--sr-purple); color: var(--sr-white); border-color: var(--sr-purple); }

/* ---- Schedule grid (mirrors the real Studieplus Ugeskema layout) ---- */
.schedule-grid {
  display: grid;
  grid-template-columns: 52px repeat(var(--day-count, 5), 1fr);
  border: 1px solid var(--sr-mid-gray);
  border-radius: 8px;
  overflow: hidden;
}
.schedule-header-cell {
  background: var(--sr-black);
  padding: 8px 6px;
  font-size: 0.8rem;
  font-weight: 600;
  text-align: center;
  border-bottom: 1px solid var(--sr-mid-gray);
  border-left: 1px solid var(--sr-mid-gray);
}
.schedule-header-cell:first-child { border-left: none; }
.absence-badge {
  display: inline-block;
  margin-top: 4px;
  padding: 2px 8px;
  border-radius: 10px;
  font-size: 0.68rem;
  font-weight: 700;
  background: #7a1f1f;
  color: #ffb4b4;
  letter-spacing: 0.02em;
}
.schedule-day-column--absent {
  background: rgba(122, 31, 31, 0.12);
}
.absence-overlay {
  position: absolute;
  left: 0;
  right: 0;
  background: repeating-linear-gradient(
    135deg,
    rgba(178, 34, 34, 0.38),
    rgba(178, 34, 34, 0.38) 8px,
    rgba(178, 34, 34, 0.26) 8px,
    rgba(178, 34, 34, 0.26) 16px
  );
  border-top: 2px solid #b91c1c;
  border-bottom: 2px solid #b91c1c;
  z-index: 2;
  pointer-events: none;
}
/* Optimistic state while a mark-absent/stop-absence write is in flight (see
   app.js's pendingAbsenceOps registry) -- swapped for the real red
   rendering once the backend confirms the write end-to-end. Always a
   freshly-injected element layered on top (z-index above the base
   .absence-overlay) rather than recoloring an existing one in place -- a
   day can have several of these at once (multiple lessons' actions
   in flight independently, see runAbsenceAction), so "cover the affected
   range" is simpler and safer than tracking which specific real element
   each pending op corresponds to. */
.absence-overlay--pending {
  z-index: 3;
  background: repeating-linear-gradient(
    135deg,
    rgba(202, 138, 4, 0.38),
    rgba(202, 138, 4, 0.38) 8px,
    rgba(202, 138, 4, 0.26) 8px,
    rgba(202, 138, 4, 0.26) 16px
  );
  border-top: 2px solid #ca8a04;
  border-bottom: 2px solid #ca8a04;
}
/* Current-time indicator on today's column (schedule grid). */
.schedule-now-line {
  position: absolute;
  left: 0;
  right: 0;
  height: 2px;
  background: var(--sr-now-line);
  box-shadow: 0 0 6px 2px rgba(var(--sr-now-line-rgb), 0.65), 0 0 16px 5px rgba(var(--sr-now-line-rgb), 0.3);
  z-index: 5;
  pointer-events: none;
}
.schedule-now-line::before {
  content: "";
  position: absolute;
  left: -4px;
  top: 50%;
  transform: translateY(-50%);
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--sr-now-line);
  box-shadow: 0 0 6px 2px rgba(var(--sr-now-line-rgb), 0.65);
}
.schedule-time-gutter {
  position: relative;
  background: var(--sr-near-black);
}
.schedule-time-label {
  position: absolute;
  right: 6px;
  transform: translateY(-50%);
  font-size: 0.7rem;
  color: var(--sr-light-gray);
}
.schedule-day-column {
  position: relative;
  border-left: 1px solid var(--sr-mid-gray);
  background-image: repeating-linear-gradient(
    to bottom,
    var(--sr-dark-gray) 0,
    var(--sr-dark-gray) 1px,
    transparent 1px,
    transparent var(--hour-px, 60px)
  );
}
.lesson-block {
  position: absolute;
  left: 3px;
  right: 3px;
  z-index: 1;
  background: var(--sr-purple-dark);
  border: 1px solid var(--sr-black);
  border-radius: 6px;
  padding: 4px 6px;
  font-size: 0.75rem;
  line-height: 1.25;
  overflow: hidden;
  cursor: pointer;
  color: var(--sr-white);
}
.lesson-block:hover { background: var(--sr-purple); }
.lesson-block .lesson-time { font-weight: 700; display: block; }
.lesson-block .lesson-note-dot {
  display: inline-block;
  width: 10px; height: 10px;
  border-radius: 50%;
  background: #4ade80;
  margin-left: 4px;
}
.lesson-block .lesson-note-dot-plain {
  background: #9ca3af;
}
.lesson-lock-icon {
  position: absolute;
  bottom: 3px;
  right: 3px;
  font-size: 11px;
  line-height: 1;
  z-index: 2;
}
.attendance-marker {
  position: absolute;
  top: 3px;
  right: 3px;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 18px; height: 18px;
  background: var(--sr-status-badge-bg);
  border: 1px solid var(--sr-black);
  border-radius: 5px;
  z-index: 2;
}
.attendance-marker--not_required {
  background: var(--sr-status-not-required-bg);
}
.attendance-marker svg { width: 14px; height: 14px; display: block; }
#lesson-detail { margin-top: 16px; }
#lesson-detail .reason-row { display: flex; gap: 8px; align-items: center; margin-top: 12px; flex-wrap: wrap; }
#lesson-detail select { width: auto; padding: 8px; border-radius: 8px; background: var(--sr-near-black); color: var(--sr-white); border: 1px solid var(--sr-mid-gray); }
#lesson-detail button { width: auto; padding: 8px 14px; }
#lesson-detail button.danger { background: #dc2626; }
#lesson-detail button.danger:hover { background: #b91c1c; }

/* ---- Grades drill-down ---- */
.subject-card {
  cursor: pointer;
  transition: background 0.15s;
}
.subject-card:hover { background: var(--sr-mid-gray); }

/* ---- Messages layout ---- */
/* The heading/compose button/search bar stay in view -- only the list (or
   the open thread, or the search results) scrolls internally. Unlike
   every other tab (short enough that scrolling the whole view is fine),
   a long message history is exactly the case worth keeping the header
   pinned for. `min-height: 0` at each nested flex level is required for
   this -- without it, a flex child defaults to sizing at least as tall as
   its own content (`min-height: auto`), which silently defeats the inner
   `overflow-y: auto` by never actually being shorter than what's inside it. */
#view-messages {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}
#messages-search-results {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
}
.messages-layout {
  display: grid;
  grid-template-columns: minmax(280px, 380px) 1fr;
  gap: 16px;
  align-items: stretch;
  flex: 1;
  min-height: 0;
}
#messages-list, #messages-thread {
  overflow-y: auto;
  min-height: 0;
}
#messages-list .card { cursor: pointer; }
#messages-list .card:hover { background: var(--sr-mid-gray); }
#messages-list .card.active { border: 1px solid var(--sr-purple); }
.thread-message {
  background: var(--sr-near-black);
  border-radius: 8px;
  padding: 10px 12px;
  margin: 10px 0;
  white-space: pre-wrap;
}
#chat-log { margin: 10px 0; }
.chat-row {
  display: flex;
  flex-direction: column;
  margin: 10px 0;
}
.chat-row--self { align-items: flex-end; }
/* Name sits ABOVE the avatar+bubble line (not a flex sibling of the
   avatar) specifically so align-items on .chat-line below only has the
   avatar and the bubble to align -- their tops land flush, instead of
   the name label's height (when it was part of the same row) pushing the
   bubble down below the avatar's top. */
.chat-name {
  font-size: 0.7rem;
  color: var(--sr-light-gray);
  margin: 0 0 2px 36px; /* 28px avatar + 8px gap, so it sits over the bubble, not the avatar */
}
.chat-line {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  max-width: 75%;
}
.chat-bubble {
  background: var(--sr-near-black);
  border-radius: 16px;
  padding: 8px 14px;
  white-space: pre-wrap;
  word-break: break-word;
}
.chat-row--self .chat-bubble { background: var(--sr-purple); color: var(--sr-white); }
.person-avatar--chat { width: 28px; height: 28px; border-radius: 50%; }
.chat-status {
  font-size: 0.7rem;
  color: var(--sr-light-gray);
  margin-top: 2px;
}
.chat-status--failed { color: var(--sr-status-absent); }
#messages-thread textarea,
#assignment-detail textarea {
  width: 100%;
  min-height: 70px;
  background: var(--sr-dark-gray);
  color: var(--sr-white);
  border: 1px solid var(--sr-mid-gray);
  border-radius: 8px;
  padding: 10px;
  font-family: inherit;
  font-size: 0.95rem;
  margin-top: 10px;
}

/* ---- Resources tree ---- */
.resources-layout {
  display: grid;
  grid-template-columns: minmax(260px, 360px) 1fr;
  gap: 16px;
  align-items: start;
}
.tree-node { user-select: none; }
.tree-node-label {
  padding: 5px 6px;
  border-radius: 6px;
  cursor: pointer;
  white-space: nowrap;
}
.tree-node-label:hover { background: var(--sr-dark-gray); }
.tree-node-label.active { background: var(--sr-purple); color: var(--sr-white); }
.tree-children { margin-left: 16px; border-left: 1px solid var(--sr-mid-gray); padding-left: 6px; }

/* ---- Modal ---- */
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.6);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 100;
}
.modal-box {
  background: var(--sr-dark-gray);
  border-radius: 12px;
  padding: 20px;
  width: 90%;
  max-width: 440px;
}
.modal-box h3 { margin-top: 0; }
/* Click-to-expand lesson note images (see app.js's openImageLightbox) --
   a dedicated overlay rather than reusing .modal-overlay above, since it
   also needs backdrop-filter: blur on the darkened background, which the
   plain compose-style modal doesn't. Own z-index above .modal-overlay's
   100 so it can never end up hidden behind another modal. */
.image-lightbox {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.75);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  z-index: 300;
  padding: 24px;
  box-sizing: border-box;
}
.image-lightbox.hidden { display: none; }
/* Fixed box every image expands into, regardless of its own natural size
   (a tiny screenshot and a huge scanned page both land at the same
   on-screen footprint) -- object-fit: contain then scales the actual
   image to fit inside that box without distorting it, letterboxing
   whichever dimension doesn't match the box's own aspect ratio instead of
   stretching. */
.image-lightbox img {
  width: 96vw;
  height: 96vh;
  max-width: 1320px;
  max-height: 960px;
  object-fit: contain;
  border-radius: 8px;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.6);
}
.modal-box textarea {
  width: 100%;
  background: var(--sr-near-black);
  color: var(--sr-white);
  border: 1px solid var(--sr-mid-gray);
  border-radius: 10px;
  padding: 10px;
  font-family: inherit;
  margin-bottom: 12px;
}
.recipient-field-wrap { position: relative; }
.recipient-chip-row {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 8px;
}
.recipient-chip-row:empty { margin-bottom: 0; }
.recipient-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: var(--sr-purple);
  color: var(--sr-white);
  border-radius: 999px;
  padding: 2px 6px 2px 10px;
  font-size: 0.8rem;
  font-weight: 600;
}
.recipient-chip .remove {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: #7a1f1f;
  color: #ffb4b4;
  font-weight: 700;
  font-size: 0.75rem;
  line-height: 1;
  cursor: pointer;
}
.recipient-chip .remove:hover { background: #a33; }
.suggestions-dropdown {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  background: var(--sr-near-black);
  border: 1px solid var(--sr-mid-gray);
  border-radius: 8px;
  margin-top: -8px;
  margin-bottom: 12px;
  max-height: 180px;
  overflow-y: auto;
  z-index: 10;
}
.suggestion-item {
  padding: 8px 12px;
  cursor: pointer;
  font-size: 0.9rem;
  display: flex;
  align-items: center;
  gap: 8px;
}
.suggestion-item:hover, .suggestion-item.highlighted { background: var(--sr-purple-dark); }
.suggestion-item .tag { color: var(--sr-light-gray); font-size: 0.8rem; }

/* ---- Absence trend chart + subject dropdown ---- */
#absence-chart-card { position: relative; }
.absence-chart-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: 12px; }
.dropdown { position: relative; display: inline-block; }
.dropdown .suggestions-dropdown { width: 260px; left: 0; right: auto; margin-top: 4px; }
.absence-subject-action { font-weight: 600; color: var(--sr-purple-light); }
.absence-subject-action:hover { background: var(--sr-purple-dark); }
.absence-subject-row { justify-content: space-between; }
.absence-subject-row input[type="checkbox"] { margin: 0; width: auto; }
.absence-swatch {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  flex-shrink: 0;
  display: inline-block;
}

/* Selected-subject chips, shown next to the dropdown toggle -- same
   color-swatch-plus-label identity as the dropdown rows/chart lines, so a
   series stays recognizable wherever it's shown. Click to deselect, same
   idea as .recipient-chip's remove affordance but the whole chip is the
   hit target here (a subject's own color IS the chip, no separate × needed
   since unchecking it in the dropdown does the same thing). */
.absence-chip-row { display: flex; flex-wrap: wrap; gap: 6px; }
.absence-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: var(--sr-dark-gray);
  border: 1px solid var(--sr-mid-gray);
  border-radius: 999px;
  padding: 4px 10px 4px 8px;
  font-size: 0.8rem;
  cursor: pointer;
}
.absence-chip:hover { border-color: var(--sr-purple); }
#absence-chart { width: 100%; }
#absence-chart svg { width: 100%; height: auto; display: block; }
.absence-chart-empty { color: var(--sr-light-gray); font-size: 0.9rem; padding: 24px 0; text-align: center; }

/* ---- Absence what-if scenario (bottom of the Absence tab) ---- */
.absence-projection-controls {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-bottom: 10px;
}
.absence-projection-controls input[type="number"] {
  width: 70px;
  padding: 8px 10px;
  border-radius: 8px;
  background: var(--sr-near-black);
  color: var(--sr-white);
  border: 1px solid var(--sr-mid-gray);
  margin-bottom: 0;
}
.absence-projection-controls input[type="number"]:disabled { opacity: 0.5; }

/* Marks numbers in the stats card that include the what-if scenario's
   hypothetical skips, so they stay visually distinct from real recorded
   data -- see renderAbsenceStats' scenario overlay. */
.absence-scenario-badge {
  display: inline-block;
  background: var(--sr-purple);
  color: var(--sr-white);
  border-radius: 999px;
  padding: 1px 8px;
  font-size: 0.7rem;
  font-weight: 700;
}
.absence-scenario-row td { background: var(--sr-mid-gray); }
.absence-chart-tooltip {
  position: absolute;
  pointer-events: none;
  background: var(--sr-near-black);
  border: 1px solid var(--sr-mid-gray);
  border-radius: 6px;
  padding: 6px 10px;
  font-size: 0.8rem;
  white-space: nowrap;
  z-index: 10;
}
/* The hover hit-zone stays exactly the avatar's original, never-resized
   footprint -- the zoomed-in copy is a separate element (see
   .avatar-zoom-overlay, wired up in app.js's wireAvatarZoom) appended
   directly to <body> and `position: fixed`, so it always renders above
   everything (including the sidebar) and is never clipped by any
   scrolling ancestor (main.content's own overflow-y: auto would otherwise
   crop a same-element transform right at its edge). Kept `pointer-events:
   none` so it can never itself be the reason this zone stops registering
   as hovered -- shrinking is driven purely by leaving THIS zone's own
   (always original-sized) box. */
.avatar-hover-zone {
  display: inline-block;
  position: relative;
  flex: none;
}
.avatar-zoom-overlay {
  position: fixed;
  pointer-events: none;
  z-index: 9999;
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  opacity: 0;
  transition: top 0.216s ease, left 0.216s ease, width 0.216s ease, height 0.216s ease, opacity 0.216s ease;
}
.person-avatar {
  width: 22px; height: 22px;
  border-radius: 50%;
  flex: none;
  background: var(--sr-status-badge-bg);
  display: flex;
  align-items: center;
  justify-content: center;
  object-fit: cover;
  overflow: hidden;
}
.person-avatar-placeholder-icon {
  width: 100%;
  filter: brightness(0); /* renders logo.png as a flat black silhouette, regardless of its own colors */
}
.person-row { display: flex; align-items: flex-start; gap: 12px; }
.person-row-body { flex: 1; min-width: 0; }
.person-avatar--large {
  width: 60px; height: auto;
  aspect-ratio: 25 / 33; /* matches Studie+'s own brugerfoto proportions (500x660) */
  border-radius: 10px;
  border: 1px solid var(--sr-black);
}
.favorite-star-btn {
  /* Overrides the global `button { width: 100% }` -- left in place, flex:
     none alone still lets flex-basis:auto fall back to that 100% width,
     making this an invisible full-row-width box (flex-shrink:0 won't let
     it shrink back down) with the star glyph merely centered inside it,
     instead of a normal content-sized button pinned to the row's edge. */
  width: auto;
  flex: none;
  align-self: flex-start;
  background: none;
  border: none;
  cursor: pointer;
  font-size: 1.4rem;
  line-height: 1;
  padding: 2px;
  color: var(--sr-light-gray);
}
.favorite-star-btn:hover { color: var(--sr-purple); }
.favorite-star-btn.active { color: var(--sr-purple); }
.favorite-star-btn:disabled { opacity: 0.5; cursor: default; }

.modal-actions { display: flex; gap: 10px; }
.modal-actions button { width: auto; padding: 10px 18px; }

/* Responsive: collapse sidebar to a top bar on narrow / phone widths */
@media (max-width: 720px) {
  #app-shell { flex-direction: column; }
  /* The fixed top-right placement (desktop) sits right on top of the nav
     bar once it collapses to a horizontal strip here -- switched to a
     normal-flow bar of its own between the nav and the content instead,
     ordered via flex rather than moving it in the HTML (it's a fixed
     overlay everywhere else, so it stays first in source order for that
     to work; `order` repositions it visually here without needing that
     to change). */
  #global-clock {
    order: 2;
    position: static;
    width: 100%;
    text-align: center;
    border-radius: 0;
    box-sizing: border-box;
  }
  nav.sidebar {
    order: 1;
    width: 100%;
    flex-direction: row;
    overflow-x: auto;
    padding: 10px;
    align-items: center;
  }
  nav.sidebar .brand { padding: 0 12px 0 4px; }
  nav.sidebar button.nav-item.active { border-left: none; border-bottom: 3px solid var(--sr-purple); }
  main.content { order: 3; padding: 18px; }
  /* The desktop reservation for the fixed top-right clock doesn't apply
     here -- below this breakpoint #global-clock is its own full-width bar
     under the nav (see above), not floating over the header row, so this
     padding would just needlessly squeeze "Messages" and the compose
     button together for no reason. */
  .view-header-row { padding-right: 0; }
  .messages-layout, .resources-layout { grid-template-columns: 1fr; }
  /* Schedule: a full week of day-columns squeezed into a phone-width grid
     is unreadable (see renderScheduleGrid in app.js) -- below this
     breakpoint it renders one day at a time instead, with its own day-nav
     row (shown here) alongside the still-present week-nav row above it, so
     both a "next day" swipe-equivalent and a "next week" jump stay
     available. */
  .day-nav { display: flex; }
  /* On a narrow screen, an open thread replaces the inbox list entirely --
     feels like a real "page" (matches the back-button-driven navigation in
     app.js's openThread/thread-back handler) instead of both stacking in
     one long scrolling column. Desktop (outside this breakpoint) keeps
     showing both side by side, unaffected -- .thread-open only means
     anything here. */
  .messages-layout.thread-open #messages-list { display: none; }
}
#thread-back { margin-bottom: 12px; }
