:root {
  /* Font weights — edit these to adjust weight across the page */
  --fw-class-name: 500;        /* "Class Name" label */
  --fw-grade-value: 600;       /* Big "##" domain grade next to the icon */
  --fw-grade-breakdown: 400;   /* "FA ##.## | SA ##.## | D ##.##" line (labels + dividers) */
  --fw-breakdown-value: 400;   /* The FA/SA/D numeric values ("##.##") in the breakdown line */
  --fw-assessment-label: 500;  /* "Formatives" / "Summatives" / "Final Exam" box titles */

  /* Core palette — change these to build a new color theme */
  --color-background: #e3e3e3;    /* page background */
  --color-surface: #ffffff;       /* card / dropdown backgrounds */
  --color-text-primary: #111111;  /* main text on surfaces */
  --color-gray-dark: #828282;     /* muted text: icon fill, breakdown text, category percentage, tab text on the page background */
  --color-gray-light: #ababab;    /* dividers, assessment box borders */
  --color-gray-lighter: #eeeeee;  /* subtle background highlight, e.g. a selected/hovered My Courses field */

  /* Accent (dropdown hover, focus outlines) */
  --color-accent: #1051cf;
  --color-accent-soft: #e6edff;
  --color-accent-strong: #2f6fed;

  /* Tab bar accent (active tab) */
  --color-tab-accent: #3a3a3a;
  --color-tab-accent-soft: #dcdcdc;
  --color-tab-accent-strong: #2b2b2b;

  /* Misc */
  --color-divider-on-background: rgba(0, 0, 0, 0.15); /* tab-bar bottom border */
  --color-scrollbar-thumb: rgba(0, 0, 0, 0.3);
  --color-shadow: rgba(149, 157, 165, 0.3);         /* cards */
  --color-shadow-popover: rgba(149, 157, 165, 0.3); /* dropdowns/calendar — floating above other content, so kept even in dark mode; see theme.css */
  --color-focus-highlight: rgba(0, 41, 107, 0.08);
  --preview-opacity: 0.35; /* live score-typing letter-grade preview — a bit higher in dark mode; see theme.css */
  --color-segment-active: #90c2ff; /* Settings segmented controls (Appearance/Date format) — same in both themes, not overridden in theme.css */
}

html {
  overscroll-behavior: none;
}

body {
  margin: 0;
  min-height: 100vh;
  background-color: var(--color-background);
  font-family: "Inter", sans-serif;
  font-optical-sizing: auto;
  font-weight: 400;
  font-style: normal;
  overscroll-behavior: none;
}

.tab-bar {
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  /* Tabs (and everything else) still sit at the bottom, matching the old
     padding: 20px 48px 0 (all the breathing room above, none below) — now
     achieved via alignment instead, since the bar's height below is fixed
     rather than sized from its content. */
  align-items: flex-end;
  gap: 4px;
  height: 71px; /* was auto-sized from .tab's own content (14px+14px padding + ~21px text) plus the 20px padding-top and 2px border-bottom below — fixed explicitly so .tab-bar-logo (align-self: stretch below) has an actual height to fill instead of its own oversized intrinsic size inflating this container */
  padding: 0 48px;
  background: var(--color-background);
  /* An inset shadow, not a real border — a real border-bottom paints
     outside the clipped scroll area (the padding box), which is a
     different layer than .tab--active::after now renders in (see its own
     bottom: 0 comment) — the two could never overlap, only sit stacked
     with a gap. This paints the same solid 2px line but inside that area
     instead, where .tab--active::after (a child, always painted after/on
     top of this) can actually cover it again, matching how it looked
     before .tab-bar needed overflow-x. */
  box-shadow: inset 0 -2px 0 0 var(--color-divider-on-background);
  /* A narrow window (or just enough tabs — the admin-only 5th one
     included) can add up to more than the viewport's width. Scrolling
     sideways here, rather than letting flex-shrink squeeze every tab down
     or wrap their text, needs every child pinned to its own natural width
     — see flex-shrink: 0 on .tab/.tab-bar-logo/.auth-container below. The
     scrollbar itself is hidden (unlike .classes-container's own visible
     one) — still scrollable by trackpad/drag, just no bar sitting right
     under the tabs' own bottom border. */
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none; /* Firefox */
}

.tab-bar::-webkit-scrollbar {
  display: none; /* Chrome/Safari/Edge */
}

/* Only one of the two logos shows at a time, based on the current theme
   (data-theme, set on <html> by the inline anti-flash script in <head> and
   kept live by preferences.js) — CSS-only, so it also follows "system"
   appearance if the OS theme changes while the page is open, no extra JS
   needed. */
.tab-bar-logo {
  align-self: stretch; /* fills .tab-bar's full (now fixed) height exactly */
  flex-shrink: 0; /* stays full-size when .tab-bar overflows, rather than getting squeezed */
  width: auto;
  margin-right: 20px;
}

.tab-bar-logo--dark {
  display: none;
}

:root[data-theme="dark"] .tab-bar-logo--light {
  display: none;
}

:root[data-theme="dark"] .tab-bar-logo--dark {
  display: block;
}

.tab {
  position: relative;
  flex-shrink: 0; /* keeps its natural width instead of squeezing/wrapping when .tab-bar overflows */
  padding: 14px 28px;
  font-size: 17px;
  font-weight: 600;
  color: var(--color-gray-dark);
  text-decoration: none;
  white-space: nowrap;
  border-radius: 12px 12px 0 0;
}

.tab:hover {
  color: var(--color-text-primary);
}

/* Revealed by auth.js (inline style) once the signed-in email is confirmed
   to be an admin — hidden by default so it never flashes for anyone else. */
.tab--admin-only {
  display: none;
}

.tab--active,
.tab--active:hover {
  background: var(--color-tab-accent-soft);
  color: var(--color-tab-accent);
}

.auth-container {
  display: flex;
  align-items: center;
  align-self: center;
  flex-shrink: 0; /* stays full-size when .tab-bar overflows, rather than getting squeezed */
  gap: 10px;
  margin-left: auto;
  padding-right: 8px;
}

.auth-signin-btn {
  padding: 8px 16px;
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  color: var(--color-text-primary);
  background: var(--color-surface);
  border: 1px solid var(--color-gray-light);
  border-radius: 999px;
  cursor: pointer;
}

.auth-signin-btn:hover {
  background: var(--color-gray-lighter);
}

.auth-avatar {
  width: 32px;
  height: 32px;
  margin-left: 16px; /* breathing room from the last tab, which now holds its own width instead of shrinking to make room */
  border-radius: 50%;
  object-fit: cover;
}

.auth-signout-btn {
  padding: 6px 14px;
  font: inherit;
  font-size: 13px;
  color: var(--color-gray-dark);
  background: none;
  border: 1px solid var(--color-gray-light);
  border-radius: 999px;
  cursor: pointer;
}

.auth-signout-btn:hover {
  background: var(--color-gray-lighter);
}

.tab--active::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  /* Was bottom: -2px, deliberately overlapping .tab-bar's border-bottom —
     but .tab-bar's overflow-x: auto (added for the horizontal-scroll fix)
     forces overflow-y to clip too (a CSS rule: one axis can't stay truly
     visible once the other isn't), which was cutting most of this bar off
     since it extended outside the box. 0 keeps the full height inside it,
     immune to that clipping either way. */
  bottom: 0;
  height: 3px;
  border-radius: 2px;
  background: var(--color-tab-accent-strong);
}

/* Centered in the space below the (fixed 71px) tab bar — see the comment
   on this element in index.html for when it's actually visible long
   enough to notice. Reuses .assessment-calendar-spinner as-is (the same
   loading icon the My Courses calendar uses), just recentered for a
   page-level rather than a card-level context. */
.domain-loading {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: calc(100vh - 71px);
}

.domain-loading[hidden] {
  display: none;
}

.classes-container {
  display: flex;
  gap: 32px;
  padding: 32px 48px;
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: thin;
  scrollbar-color: var(--color-scrollbar-thumb) transparent;
}

.classes-container::-webkit-scrollbar {
  height: 2px;
}

.classes-container::-webkit-scrollbar-track {
  background: transparent;
}

.classes-container::-webkit-scrollbar-thumb {
  background: var(--color-scrollbar-thumb);
  border-radius: 0;
}

.page-content {
  padding: 64px 48px;
  padding-top: 32px;
}

/* My Courses only — not a plain 0, since box-shadow's ink overflow doesn't
   extend the page's scrollable height. With no bottom clearance at all, a
   card tall enough to need page scrolling would have the last few pixels
   of its own shadow (0 8px 24px, an 8px offset + 24px blur = 32px past the
   card's edge) sitting below the max scroll position — never scrollable
   into view, effectively clipped even though nothing sets overflow:
   hidden. 32px is just enough room for that shadow to fully render. */
.page-content--flush-bottom {
  padding-bottom: 32px;
}

/* Fixed to the viewport (not the end of the page's own content) so it
   never adds scrollable height of its own — just sits there as a
   permanent label, the same regardless of how tall the page above it is.
   pointer-events: none since it's purely decorative and shouldn't ever
   intercept a click meant for whatever happens to scroll underneath it. */
.page-footer {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 12px;
  margin: 0;
  font-size: 12px;
  color: var(--color-gray-dark);
  text-align: center;
  pointer-events: none;
}

.placeholder-card {
  max-width: 480px;
  background: var(--color-surface);
  border-radius: 20px;
  padding: 32px;
  box-shadow: 0 8px 24px var(--color-shadow);
}

.placeholder-card h2 {
  margin: 0 0 8px;
  font-size: 28px;
  font-weight: var(--fw-class-name);
  color: var(--color-text-primary);
}

.placeholder-card p {
  margin: 0;
  font-size: 16px;
  color: var(--color-gray-dark);
}

/* Reminder toast — shown (see showSignInToast in shared.js) whenever a
   signed-out edit tries to save. Floats above the page like the dropdowns/
   date calendar, so it uses their shadow token (kept, just lighter, in
   dark mode) rather than a flat card's (dropped entirely there). */
.signin-toast {
  position: fixed;
  left: 50%;
  bottom: 32px;
  z-index: 100;
  transform: translate(-50%, 16px);
  opacity: 0;
  pointer-events: none;
  max-width: calc(100% - 64px);
  background: var(--color-surface);
  color: var(--color-text-primary);
  padding: 20px 32px;
  border-radius: 20px;
  box-shadow: 0 8px 24px var(--color-shadow-popover);
  font-size: 17px;
  text-align: center;
  transition: transform 0.25s ease, opacity 0.25s ease;
}

.signin-toast--visible {
  transform: translate(-50%, 0);
  opacity: 1;
}

/* Blocks the page (see promptForDecryptionCode in encryption.js) until a
   working decryption code is entered — deliberately no close/skip control
   anywhere here, since dismissing it without the right key is exactly the
   mistake it exists to prevent. */
.decryption-prompt-overlay {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: rgba(0, 0, 0, 0.5);
}

.decryption-prompt {
  width: 100%;
  max-width: 440px;
  display: flex;
  flex-direction: column;
  gap: 16px;
  background: var(--color-surface);
  border-radius: 20px;
  padding: 32px;
  box-shadow: 0 8px 24px var(--color-shadow-popover);
}

.decryption-prompt-title {
  margin: 0;
  font-size: 22px;
  font-weight: var(--fw-class-name);
  color: var(--color-text-primary);
}

.decryption-prompt-text {
  margin: 0;
  font-size: 14px;
  line-height: 1.5;
  color: var(--color-gray-dark);
}

.decryption-prompt-input {
  font: inherit;
  font-size: 15px;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  color: var(--color-text-primary);
  background: var(--color-gray-lighter);
  border: none;
  border-radius: 10px;
  padding: 12px 14px;
}

.decryption-prompt-input:focus {
  outline: 2px solid var(--color-accent-strong);
  outline-offset: -2px;
}

.decryption-prompt-error {
  margin: 0;
  font-size: 14px;
  color: #e5484d;
}

.decryption-prompt-submit {
  padding: 12px 20px;
  border: none;
  border-radius: 999px;
  background: var(--color-accent-strong);
  color: #ffffff;
  font: inherit;
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
}

.decryption-prompt-submit:hover {
  opacity: 0.9;
}

.decryption-prompt-submit:disabled {
  opacity: 0.6;
  cursor: default;
}

.decryption-prompt-scan-btn {
  padding: 12px 20px;
  border: 1px solid var(--color-gray-light);
  border-radius: 999px;
  background: transparent;
  color: var(--color-text-primary);
  font: inherit;
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
}

.decryption-prompt-scan-btn:hover {
  background: var(--color-gray-lighter);
}

.decryption-prompt-camera {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.decryption-prompt-video {
  width: 100%;
  border-radius: 12px;
  background: #000000;
  /* The back camera's raw feed isn't mirrored, but object-fit: cover
     avoids letterboxing regardless of the device's actual aspect ratio. */
  aspect-ratio: 1;
  object-fit: cover;
}

.decryption-prompt-camera-cancel {
  padding: 10px 20px;
  border: none;
  border-radius: 999px;
  background: var(--color-gray-lighter);
  color: var(--color-text-primary);
  font: inherit;
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
}

.decryption-prompt-camera-cancel:hover {
  background: var(--color-gray-light);
}

.settings-card {
  max-width: 560px;
  display: flex;
  flex-direction: column;
  gap: 22px;
  background: var(--color-surface);
  border-radius: 20px;
  padding: 32px;
  box-shadow: 0 8px 24px var(--color-shadow);
}

.settings-title {
  margin: 0;
  font-size: 28px;
  font-weight: var(--fw-class-name);
  color: var(--color-text-primary);
}

.settings-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
}

.settings-label {
  font-size: 17px;
  color: var(--color-text-primary);
}

.settings-divider {
  height: 1px;
  background: var(--color-gray-lighter);
}

/* Appearance / Font / Date format pickers. */
.segmented-control {
  position: relative; /* containing block for .segmented-thumb below */
  display: inline-flex;
  gap: 4px;
  padding: 4px;
  background: var(--color-gray-lighter);
  border-radius: 999px;
}

/* The active pill background, as a single element that slides between
   options instead of each option's own background snapping on and off —
   see initSegmentedControl in settings.js. Sits behind the option buttons
   (z-index), so their text/hover states are unaffected. */
.segmented-thumb {
  position: absolute;
  top: 4px;
  bottom: 4px;
  left: 0;
  border-radius: 999px;
  background: var(--color-segment-active);
  transition: left 0.2s ease, width 0.2s ease;
}

.segmented-option {
  position: relative;
  z-index: 1;
  padding: 8px 16px;
  border: none;
  border-radius: 999px;
  background: transparent;
  color: var(--color-text-primary);
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
}

.segmented-option:hover:not(.segmented-option--active) {
  background: var(--color-gray-light);
}

.segmented-option--active {
  color: #111111;
}

.settings-danger-row {
  display: flex;
  gap: 12px;
}


.settings-danger-btn {
  padding: 10px 20px;
  border: none;
  border-radius: 999px;
  background: var(--color-gray-lighter);
  color: var(--color-text-primary);
  font: inherit;
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
}

.settings-danger-btn:hover {
  background: var(--color-gray-light);
}

/* Row of up to two boxes: .settings-card and (once "Login Keys" is
   clicked — see settings.js) .login-keys-card beside it. align-items:
   flex-start so the second box doesn't stretch to match the first one's
   height when it's shorter. */
.settings-layout {
  display: flex;
  align-items: flex-start;
  gap: 24px;
}

/* Reuses .settings-card's own box styling (background/padding/radius/
   shadow/flex-column-with-gap) — this class only overrides the width,
   since its content (a code + a QR) needs far less room than the full
   settings list. */
.login-keys-card {
  max-width: 360px;
}

.settings-encryption-hint {
  margin: 0;
  font-size: 14px;
  line-height: 1.5;
  color: var(--color-gray-dark);
}

.settings-encryption-code-row {
  display: flex;
  gap: 12px;
}

.settings-encryption-code {
  flex: 1;
  min-width: 0;
  font: inherit;
  font-size: 14px;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  color: var(--color-text-primary);
  background: var(--color-gray-lighter);
  border: none;
  border-radius: 10px;
  padding: 10px 14px;
}

.settings-encryption-code:focus {
  outline: 2px solid var(--color-accent-strong);
  outline-offset: -2px;
}

.settings-copy-btn {
  padding: 10px 20px;
  border: none;
  border-radius: 999px;
  background: var(--color-gray-lighter);
  color: var(--color-text-primary);
  font: inherit;
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
}

.settings-copy-btn:hover {
  background: var(--color-gray-light);
}

/* Empty (no inline SVG yet) until settings.js's renderEncryptionQr fills
   it in — collapses to nothing rather than showing a blank white box. A
   QR code needs real light-on-dark contrast to scan reliably, so this
   stays a fixed white card regardless of theme, unlike everything else on
   this page. */
.settings-encryption-qr:empty {
  display: none;
}

.settings-encryption-qr {
  align-self: flex-start;
  background: #ffffff;
  padding: 12px;
  border-radius: 12px;
}

.settings-encryption-qr svg {
  display: block;
  width: 160px;
  height: 160px;
}

.toggle-switch {
  position: relative;
  display: inline-block;
  flex-shrink: 0;
  width: 44px;
  height: 24px;
}

.toggle-switch input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}

.toggle-slider {
  position: absolute;
  inset: 0;
  background: var(--color-gray-light);
  border-radius: 999px;
  cursor: pointer;
  transition: background-color 0.15s ease;
}

.toggle-slider::before {
  content: "";
  position: absolute;
  left: 3px;
  top: 3px;
  width: 18px;
  height: 18px;
  background: var(--color-surface);
  border-radius: 50%;
  transition: transform 0.15s ease;
}

.toggle-switch input:checked + .toggle-slider {
  background: var(--color-accent);
}

.toggle-switch input:checked + .toggle-slider::before {
  transform: translateX(20px);
}

.toggle-switch input:focus-visible + .toggle-slider {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* Applied briefly by settings.js's initToggle while it sets a switch's
   initial checked state from the saved preference — without this, that
   assignment triggers the same transition as a real click, so every switch
   already on would visibly animate on from off the moment the page loads
   instead of just showing its actual state. */
.toggle-switch--instant .toggle-slider,
.toggle-switch--instant .toggle-slider::before {
  transition: none;
}

.schedule-container {
  display: flex;
  align-items: stretch;
  gap: 32px;
}

.schedule-cards-column {
  display: flex;
  flex-direction: column;
  gap: 32px;
}

/* Same visual language as .class-card (background, radius, shadow) — just
   a different width/padding since this card's content is a form, not a
   fixed-height grade summary. */
.schedule-card {
  width: 540px;
  background: var(--color-surface);
  border-radius: 20px;
  padding: 24px 32px 32px;
  box-shadow: 0 8px 24px var(--color-shadow);
}

/* Matches .class-name's size/weight/color; centered instead of left-aligned. */
.schedule-title {
  margin: 0 0 16px;
  font-size: 28px;
  font-weight: var(--fw-class-name);
  color: var(--color-text-primary);
  text-align: center;
}

.schedule-row {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 10px 0;
  /* The row's own padding is what visually separates one course from the
     next, so making the whole row show the text-edit cursor (not just the
     input itself) keeps that gap feeling like part of the same text field. */
  cursor: text;
}

/* Matches .assessment-label's size/weight; grayed out like a label. */
.schedule-label {
  flex: 0 0 120px;
  font-size: 17px;
  font-weight: var(--fw-assessment-label);
  color: var(--color-gray-dark);
}

.schedule-field {
  position: relative;
  flex: 1;
  border-radius: 10px;
  padding: 6px 10px;
  transition: background-color 0.15s ease;
}

/* Hover previews the highlight; :focus-within keeps it lit for whichever
   field is actually selected, independent of where the mouse currently is —
   so only the selected box stays lit even while hovering a different one. */
.schedule-row:hover .schedule-field,
.schedule-field:focus-within {
  background: var(--color-gray-lighter);
}

/* Matches .score-value's size/color — same "editable value" text style. */
.schedule-input {
  width: 100%;
  font: inherit;
  font-size: 22px;
  color: var(--color-text-primary);
  border: none;
  background: transparent;
  padding: 0 26px 0 0;
  margin: 0;
}

.schedule-input::placeholder {
  color: var(--color-gray-light);
}

.schedule-input:focus {
  outline: none;
}

/* Clears the field. Only shown while hovering a row that actually has a
   value typed in it. */
.schedule-clear-btn {
  display: none;
  position: absolute;
  right: 6px;
  top: 50%;
  transform: translateY(-50%);
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border: none;
  background: none;
  color: var(--color-gray-dark);
  font-size: 30px;
  line-height: 1;
  cursor: pointer;
  padding: 0;
}

.schedule-row:hover .schedule-field.has-value .schedule-clear-btn {
  display: flex;
}

.schedule-dropdown {
  position: absolute;
  z-index: 10;
  top: calc(100% + 8px);
  left: 0;
  right: 0;
  margin: 0;
  padding: 4px;
  list-style: none;
  background: var(--color-surface);
  border-radius: 12px;
  box-shadow: 0 8px 24px var(--color-shadow-popover);
  max-height: 240px;
  overflow-y: auto;
}

.schedule-dropdown:empty {
  display: none;
}

.schedule-dropdown-item {
  position: relative;
  padding: 8px 12px;
  font-size: 17px;
  color: var(--color-text-primary);
  border-radius: 8px;
  cursor: pointer;
}

.schedule-dropdown-item:hover,
.schedule-dropdown-item--highlighted {
  background: var(--color-accent-soft);
  color: var(--color-accent);
}

/* Marks whichever item Enter would actually pick: the keyboard-highlighted
   one, or — if nothing's been arrow-key-highlighted yet — the first item
   (matches the "Enter selects the first result" behavior in app.js). */
.schedule-dropdown-item--highlighted::after,
.schedule-dropdown:not(:has(.schedule-dropdown-item--highlighted)) .schedule-dropdown-item:first-child::after {
  content: "Enter";
  position: absolute;
  right: 8px;
  top: 50%;
  transform: translateY(-50%);
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--color-gray-lighter);
  color: var(--color-gray-dark);
  font-size: 12px;
}

/* Arrow-key highlighting turns the item's own background blue
   (--color-accent-soft, see the rule above) — swap the badge to white so
   it still reads clearly against that. */
.schedule-dropdown-item--highlighted::after {
  background: var(--color-surface);
}

/* --- Division picker --- */
/* A row of small circles for courses that split into several sections
   testing on different days (e.g. English 9/10/11/12, Chinese I-IV — see
   COURSE_CATALOG's own `divisions`) — used on My Courses (one per period
   row, next to its .schedule-field, shown/populated by syncDivisionPicker
   in my-courses.js) and on the Assessments admin page (one, in the
   add-form, shown/populated by syncAdminDivisionPicker in
   assessments-admin.js). position: relative here is only for
   .division-picker-label below (My Courses' "Select your division:" hint,
   shown only while nothing's picked yet — the admin page never adds one,
   since .admin-add-label already says "Division" above it), which needs
   this as its containing block. Same [hidden]-vs-author-style reasoning
   as .admin-add-field[hidden] above — display: flex here needs its own
   override to let the attribute actually hide it. */
.division-picker {
  position: relative; /* containing block for .division-picker-label below */
  display: flex;
  align-items: center;
  gap: 6px;
  flex-shrink: 0;
}

.division-picker[hidden] {
  display: none;
}

/* Absolutely positioned (taken out of the flex flow entirely) so it floats
   above the circles as an overlay rather than pushing them down — its
   presence/absence (only shown while nothing's picked yet) never changes
   where the circles themselves sit. */
.division-picker-label {
  position: absolute;
  left: 0;
  bottom: 100%;
  margin-bottom: 4px;
  font-size: 12px;
  color: var(--color-gray-dark);
  white-space: nowrap;
}

.division-option {
  width: 34px;
  height: 34px;
  flex-shrink: 0;
  border: none;
  border-radius: 50%;
  background: var(--color-gray-lighter);
  color: var(--color-text-primary);
  font: inherit;
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}

.division-option:hover:not(.division-option--selected) {
  background: var(--color-gray-light);
}

.division-option--selected {
  background: var(--color-accent-strong);
  color: #ffffff;
}

/* --- Assessment calendar --- */
/* Shared by two pages with different interaction models: on My Courses
   (see my-courses.js), a read-only Google Calendar-style grid where each
   assessment is its own small pill directly on its day, no click needed;
   on the Assessments admin page (see assessments-admin.js), the classic
   month-grid where a day only reveals its assessments — and lets you add
   more — once clicked. Both get an identical fixed pixel WIDTH here, same
   approach as .schedule-card's own 540px, rather than sizing it off the
   viewport — a wide viewport shouldn't grow every day cell along with the
   card. HEIGHT is deliberately left off this shared rule: on My Courses,
   align-items: stretch on .schedule-container (must never exceed) sizes
   this card to match the real, current height of the A-Day/B-Day column
   next to it — see the admin-only override further down for how the
   Assessments page, which has no such sibling, instead gets a fixed
   height of its own. .assessment-calendar-weeks' own flex: 1 +
   overflow-y: auto below absorbs whatever's left over inside that height,
   scrolling internally (not the whole page) if the day's content doesn't
   fit — and align-content: start on it (see that rule) keeps its rows at
   their natural size instead of stretching to fill the space. */

.assessment-calendar-card {
  width: 560px;
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  background: var(--color-surface);
  border-radius: 20px;
  padding: 24px 32px 32px;
  box-shadow: 0 8px 24px var(--color-shadow);
}

.assessment-calendar-title {
  margin: 0 0 16px;
  font-size: 28px;
  font-weight: var(--fw-class-name);
  color: var(--color-text-primary);
}

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

.assessment-calendar-label {
  font-size: 18px;
  font-weight: var(--fw-assessment-label);
  color: var(--color-text-primary);
}

.assessment-calendar-nav-btn {
  width: 30px;
  height: 30px;
  border: none;
  border-radius: 50%;
  background: none;
  color: var(--color-text-primary);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
}

.assessment-calendar-nav-btn:hover:not(:disabled) {
  background: var(--color-gray-lighter);
}

.assessment-calendar-nav-btn:disabled {
  color: var(--color-gray-light);
  cursor: default;
}

.assessment-calendar-weekday-row,
.assessment-calendar-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 6px;
}

.assessment-calendar-weekday-row {
  margin-bottom: 6px;
}

/* display: grid above is an author style, which beats the [hidden]
   attribute's own default (display: none) UA style regardless of
   specificity — needed on My Courses now that this stays hidden until the
   assessments collection actually loads (see initAssessmentCalendar in
   my-courses.js). Harmless for the Assessments admin page's own use of
   this class, which never gets the hidden attribute. */
.assessment-calendar-weekday-row[hidden] {
  display: none;
}

.assessment-calendar-weekday-row span {
  text-align: center;
  font-size: 13px;
  color: var(--color-gray-dark);
}

/* My Courses' own assessment calendar skips weekends (assessments are only
   ever on school days) — 5 wider columns instead of the 7 the Assessments
   admin page's month-grid still uses (see .assessment-calendar-weekday-row
   above, shared by both). */
.assessment-calendar-weekday-row--5col {
  grid-template-columns: repeat(5, 1fr);
}

.assessment-calendar-day {
  position: relative;
  width: 100%;
  aspect-ratio: 1;
  border: none;
  border-radius: 12px;
  background: none;
  font: inherit;
  font-size: 15px;
  color: var(--color-text-primary);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}

.assessment-calendar-day--outside {
  visibility: hidden;
  cursor: default;
}

/* :not(...--selected) matters here: .assessment-calendar-day:hover (a
   class + a pseudo-class) is more specific than .assessment-calendar-
   day--selected (a single class) below, so without this exclusion,
   hovering an already-selected day would override its blue/white
   background back to gray while leaving its white text untouched —
   unreadable in light mode, and dark-on-dark in dark mode (--color-
   surface, the selected state's text color, is also a dark gray there).
   The selected state's own colors already have correct contrast in both
   themes, so hovering it should just leave them alone. */
.assessment-calendar-day:hover:not(.assessment-calendar-day--selected) {
  background: var(--color-gray-lighter);
}

/* Same small fixed-size circle as .assessment-week-day-number--today on
   My Courses — sized around just the number (see the dayNumber span in
   renderAdminCalendar), not the whole (much bigger) cell button. */
.assessment-calendar-day-number--today {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: var(--color-accent-strong);
  color: #ffffff;
  font-weight: 700;
}

/* Today's circle, when that same day is also selected: swaps to the exact
   inverse of .assessment-calendar-day--selected's own background/color
   pair (var(--color-accent) blue-in-light/white-in-dark bg + var(--color-
   surface) white-in-light/dark-gray-in-dark text) rather than staying its
   usual --color-accent-strong blue — against the now-solid selected
   background, blue-on-blue (or, in dark mode, near-white-on-white) left
   the circle barely visible. */
.assessment-calendar-day--selected .assessment-calendar-day-number--today {
  background: var(--color-surface);
  color: var(--color-accent);
}

.assessment-calendar-day--selected {
  background: var(--color-accent);
  color: var(--color-surface);
}

/* Small dot under the day number, marking at least one of the student's
   courses has an assessment that day. */
.assessment-calendar-day--has-entries::after {
  content: "";
  position: absolute;
  bottom: 6px;
  left: 50%;
  transform: translateX(-50%);
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--color-accent-strong);
}

.assessment-calendar-day--selected.assessment-calendar-day--has-entries::after {
  background: var(--color-surface);
}

/* Still used by the Assessments admin page's day-detail panel (see
   .admin-day-entry in the admin section below) — the type badge next to
   an entry in that click-to-manage list, not this page's own pills. */
.assessment-calendar-entry-type {
  flex-shrink: 0;
  padding: 3px 10px;
  border-radius: 999px;
  background: var(--color-accent-soft);
  color: var(--color-accent);
  font-size: 12px;
  font-weight: 600;
}

/* One flat grid — every day cell (across all 4 weeks) is a direct child in
   document order, 5 per row, CSS grid auto-wraps them into rows on its
   own. This (rather than a wrapper <div> per week) is what lets a single
   gap + background pair draw gray gridlines uniformly between every cell,
   both across a week and down between weeks: gap: 1px leaves a 1px seam
   between cells, and since each .assessment-week-day paints its own
   opaque background (--color-surface) while this container's own
   background shows through only in that seam, the seam reads as a thin
   gray line. Scrolls internally (not the page) if all 4 weeks don't fit —
   see the .assessment-calendar-card comment above for how its height is
   pinned to match the A-Day/B-Day column. */
.assessment-calendar-weeks {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  /* align-content defaults to "stretch" — left alone here on purpose, so
     the 4 week-rows grow to fill whatever height .assessment-calendar-card
     actually ends up being (matched to the A-Day/B-Day column, see that
     rule's own comment), rather than sitting at their bare min-height and
     leaving the rest of the box empty. .assessment-calendar-legend below
     is a separate, non-growing flex sibling specifically so it keeps its
     own fixed height and isn't swallowed into this stretch. */
  gap: 1px;
  background: var(--color-gray-lighter);
  border: 1px solid var(--color-gray-lighter);
  scrollbar-width: thin;
  scrollbar-color: var(--color-scrollbar-thumb) transparent;
}

.assessment-calendar-weeks::-webkit-scrollbar {
  width: 4px;
}

.assessment-calendar-weeks::-webkit-scrollbar-track {
  background: transparent;
}

.assessment-calendar-weeks::-webkit-scrollbar-thumb {
  background: var(--color-scrollbar-thumb);
  border-radius: 4px;
}

/* display: grid above is an author style, which beats the [hidden]
   attribute's own default (display: none) UA style regardless of
   specificity — same gotcha already worked around for .admin-add-field
   and .division-picker elsewhere. Needed now that this stays hidden until
   the assessments collection actually loads (see initAssessmentCalendar
   in my-courses.js). */
.assessment-calendar-weeks[hidden] {
  display: none;
}

/* min-width: 0 overrides a grid item's default min-width: auto (its
   content's own min-content size) — without it, a pill inside that refuses
   to shrink (see the matching comment on .assessment-pill below) could
   force this cell wider than its actual 1fr track, spilling into the next
   column and throwing the whole row out of alignment with the day-number
   row above it. */
.assessment-week-day {
  background: var(--color-surface);
  min-width: 0;
  min-height: 90px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 6px 4px;
}

.assessment-week-day-number {
  align-self: flex-start;
  width: 22px;
  height: 22px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 13px;
  color: var(--color-text-primary);
}

.assessment-week-day-number--today {
  border-radius: 50%;
  background: var(--color-accent-strong);
  color: #ffffff;
  font-weight: 700;
}

.assessment-week-day-pills {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 3px;
}

/* Narrow columns (5 across a fixed-width card) mean a long name can still
   get truncated — see the title attribute set in buildAssessmentPill for the
   full text on hover. Color-coded by type instead of labeling it in text,
   same idea as Google Calendar's own per-calendar event colors.
   min-width: 0 is the fix that actually makes text-overflow: ellipsis
   below take effect: a flex item's default min-width is auto (its
   content's own min-content size), so a long name like "Global Studies"
   would otherwise refuse to shrink and overflow its column instead of
   truncating, throwing off alignment with the day-number row above it. */
.assessment-pill {
  display: block;
  min-width: 0;
  padding: 3px 8px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  cursor: default;
  /* Default for any type without its own modifier below — a custom
     admin-typed "Other" label (see assessments-admin.js) would otherwise
     render with no background at all. */
  background: var(--color-gray-lighter);
  color: var(--color-text-primary);
}

.assessment-pill--formative {
  background: var(--color-accent-soft);
  color: var(--color-accent);
}

/* --color-accent-strong (not --color-accent, which turns white in dark
   mode — see the matching comment on .admin-add-submit-btn) stays blue in
   both themes, which a solid pill with white text actually needs. */
.assessment-pill--summative {
  background: var(--color-accent-strong);
  color: #ffffff;
}

/* Key for the two pill colors above — sits below the grid, reusing the
   exact same .assessment-pill classes (not a lookalike copy) so it's
   guaranteed to always match however those actually render. A plain,
   non-growing flex row (not part of .assessment-calendar-weeks' own
   flex: 1) — it keeps its own natural content height, which is what
   .assessment-calendar-weeks' flex: 1 sizes itself around. */
.assessment-calendar-legend {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 16px;
}

/* display: flex above is an author style, which beats the [hidden]
   attribute's own default (display: none) UA style regardless of
   specificity — needed now that this stays hidden until the assessments
   collection actually loads (see initAssessmentCalendar in my-courses.js). */
.assessment-calendar-legend[hidden] {
  display: none;
}

/* Fills the same flex: 1 role .assessment-calendar-weeks normally does
   (see that rule's own comment on why it's flex: 1), so the card doesn't
   change height between "loading" and "loaded" — just centers a spinner
   in that same space instead of the grid. */
.assessment-calendar-loading {
  display: flex;
  flex: 1;
  align-items: center;
  justify-content: center;
}

.assessment-calendar-loading[hidden] {
  display: none;
}

.assessment-calendar-spinner {
  width: 32px;
  height: 32px;
  border: 3px solid var(--color-gray-light);
  border-top-color: var(--color-gray-dark);
  border-radius: 50%;
  /* Not gated by Settings > Animations — same as .paste-spinner on the GPA
     page, a loading indicator is functional feedback, not decoration. */
  animation: assessment-calendar-spin 0.7s linear infinite;
}

@keyframes assessment-calendar-spin {
  to {
    transform: rotate(360deg);
  }
}

/* --- Assessments admin page --- */
/* Reuses the .assessment-calendar-* classes above for the calendar itself
   (identical visual component) — this section only covers what's unique to
   the admin page: the two-column layout and the day-detail/add-entry panel. */

.admin-not-authorized {
  max-width: 480px;
  background: var(--color-surface);
  border-radius: 20px;
  padding: 32px;
  box-shadow: 0 8px 24px var(--color-shadow);
  font-size: 17px;
  color: var(--color-text-primary);
}

.admin-calendar-layout {
  display: flex;
  align-items: stretch;
  gap: 32px;
}

/* Width comes from the shared .assessment-calendar-card rule as-is, same
   as My Courses' own — only height is set here, since this page (unlike
   My Courses) has no A-Day/B-Day-style sibling to stretch-match against.
   align-items: stretch above sizes .admin-day-panel (no explicit height of
   its own) to match this exactly, so both boxes stay the same height. */
.admin-calendar-layout .assessment-calendar-card {
  height: 600px;
}

.admin-day-panel {
  flex: 1;
  min-width: 320px;
  display: flex;
  flex-direction: column;
  background: var(--color-surface);
  border-radius: 20px;
  padding: 24px 32px 32px;
  box-shadow: 0 8px 24px var(--color-shadow);
}

.admin-day-panel-title {
  margin: 0 0 16px;
  font-size: 22px;
  font-weight: var(--fw-class-name);
  color: var(--color-text-primary);
}

/* flex: 1 + overflow-y: auto — same reasoning as .score-list elsewhere in
   this app: now that .admin-day-panel has a fixed height (stretched to
   match .assessment-calendar-card's own, see the comment there), a day
   with a lot of entries needs somewhere to go that isn't "overflow the
   card" — scroll internally, keeping the add-form below always visible
   rather than pushed off the bottom. */
.admin-day-entry-list {
  list-style: none;
  margin: 0 0 20px;
  padding: 0;
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
  overflow-y: auto;
}

.admin-day-entry-empty {
  font-size: 14px;
  color: var(--color-gray-dark);
}

.admin-day-entry {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 0;
}

.admin-day-entry + .admin-day-entry {
  border-top: 1px solid var(--color-gray-lighter);
}

.admin-day-entry-name {
  flex: 1;
  font-size: 15px;
  color: var(--color-text-primary);
}

.admin-day-entry-delete {
  flex-shrink: 0;
  width: 26px;
  height: 26px;
  border: none;
  border-radius: 50%;
  background: none;
  color: var(--color-gray-dark);
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
}

.admin-day-entry-delete:hover {
  background: var(--color-gray-lighter);
  color: var(--color-text-primary);
}

.admin-add-form {
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding-top: 16px;
  border-top: 1px solid var(--color-gray-lighter);
}

.admin-add-field {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

/* display: flex above is an author style, which beats the [hidden]
   attribute's own default (display: none) UA style regardless of
   specificity — same gotcha already worked around once for
   .gpa-summary-item[hidden]. Needed for #admin-division-field, only
   revealed once the typed course actually has divisions. */
.admin-add-field[hidden] {
  display: none;
}

.admin-add-label {
  font-size: 13px;
  font-weight: 600;
  color: var(--color-gray-dark);
}

/* .admin-course-field reuses .schedule-field's look, but that class assumes
   a .schedule-row ancestor for its hover highlight — give it a resting
   border of its own here instead, since it stands alone in this form. */
.admin-course-field {
  height: 50px;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  border: 1px solid var(--color-gray-light);
}

/* Scoped to this one instance (not the base .segmented-control, which
   Settings' Appearance/Date format pickers also use and shouldn't change)
   — matches .admin-course-field/.admin-add-submit-btn's own fixed height
   so all three controls in this form line up exactly. */
#admin-type-toggle {
  height: 50px;
  box-sizing: border-box;
  align-items: center;
}

.admin-type-row {
  display: flex;
  align-items: center;
  gap: 10px;
}

/* Only shown (see the hidden attribute toggled in setupTypeToggle) while
   "Other" is the active segmented option — same fixed height as the
   segmented control it sits beside. */
.admin-type-other-input {
  height: 50px;
  box-sizing: border-box;
  flex: 1;
  min-width: 0;
  padding: 0 14px;
  border: 1px solid var(--color-gray-light);
  border-radius: 999px;
  background: var(--color-surface);
  color: var(--color-text-primary);
  font: inherit;
  font-size: 15px;
}

.admin-type-other-input:focus {
  outline: none;
  border-color: var(--color-accent-strong);
}

/* --color-accent itself turns white in dark mode (theme.css) — used
   elsewhere for hover text/focus rings against a dark surface, but wrong
   for a solid filled button. --color-accent-strong stays blue in both
   themes, which is what a white-text button actually needs. */
.admin-add-submit-btn {
  height: 50px;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 999px;
  background: var(--color-accent-strong);
  color: #ffffff;
  font: inherit;
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
}

.admin-add-submit-btn:hover {
  opacity: 0.9;
}

/* Shown next to the two sample Domain cards until the user has entered a
   real course in My Courses. */
.domain-empty-hint {
  align-self: center;
  flex: 0 0 380px;
  margin: 0;
  background: var(--color-surface);
  border-radius: 20px;
  padding: 64px 40px;
  text-align: center;
  font-size: 22px;
  color: var(--color-text-primary);
  box-shadow: 0 8px 24px var(--color-shadow);
}

.domain-empty-hint a {
  color: var(--color-accent);
  text-decoration: underline;
}

.class-card {
  flex: 0 0 380px;
  display: flex;
  flex-direction: column;
  background: var(--color-surface);
  border-radius: 20px;
  padding: 24px 32px 32px;
  /* min-height (a floor), not a hard height — a card whose content
     (namely .next-assessment-line, when present) doesn't fit in 520px
     grows taller instead of squeezing .assessment-groups' flex: 1 share
     down to make room. .classes-container's default align-items: stretch
     then does the rest: every card in the row is stretched to match
     whichever one ended up tallest, so a neighboring card without an
     assessment line grows right along with it rather than sitting shorter
     next to it. */
  min-height: 520px;
  box-shadow: 0 8px 24px var(--color-shadow);
}

.class-name {
  margin: 0 0 4px;
  font-size: 28px;
  font-weight: var(--fw-class-name);
  color: var(--color-text-primary);
}

.grade-summary {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 12px;
}

.letter-grade {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  object-fit: cover;
  flex-shrink: 0;
}

.letter-grade--lg {
  width: 54px;
  height: 54px;
}

.letter-grade--empty {
  background: transparent;
}

/* Live preview of the letter grade a score would become, shown while still
   typing it (before commit) — see updateScorePreview in domain.js.
   Desaturated (not just faded) so it reads as a muted placeholder rather
   than a dim version of the real, colored icon. */
.letter-grade--preview {
  opacity: var(--preview-opacity);
  filter: grayscale(1);
}

.grade-value {
  font-size: 44px;
  font-weight: var(--fw-grade-value);
  color: var(--color-text-primary);
}

.grade-breakdown {
  margin: 0 0 20px;
  font-size: 15px;
  font-weight: var(--fw-grade-breakdown);
  color: var(--color-gray-dark);
}

/* Tightens the gap to .next-assessment-line specifically when it's
   actually showing something (:not(:empty)) — its own 20px margin-bottom
   below still keeps the usual distance from the score boxes underneath,
   this only pulls the two text lines themselves closer together. Skipped
   while the line itself is hidden (see data-hide-upcoming-assessments
   below) — nothing to tighten toward then. */
html:not([data-hide-upcoming-assessments]) .grade-breakdown:has(+ .next-assessment-line:not(:empty)) {
  margin-bottom: 6px;
}

/* Set on <html> by preferences.js (Settings > Grade breakdown off). */
html[data-hide-breakdown] .grade-breakdown {
  display: none;
}

/* Set on <html> by preferences.js (Settings > Show upcoming assessments
   off). */
html[data-hide-upcoming-assessments] .next-assessment-line {
  display: none;
}

.grade-breakdown .divider {
  margin: 0;
  color: var(--color-gray-light);
}

.grade-breakdown .breakdown-value {
  font-weight: var(--fw-breakdown-value);
  color: var(--color-text-primary);
}

/* "Next Summative: Aug 18" — the nearest upcoming assessment for this
   course, from the school-wide calendar admins maintain on the Assessments
   page (see assessments-shared.js/updateNextAssessmentLine in domain.js).
   Empty (and collapsed) when there's nothing upcoming. Controlled by its
   own Settings > Show upcoming assessments toggle, independent of Grade
   breakdown — see the data-hide-upcoming-assessments rule below. */
/* Same size/weight/color as .grade-breakdown (the grade-breakdown line
   right above it, e.g. its own "FA -" labels) rather than its own bolder,
   smaller, blue-accented style. */
.next-assessment-line {
  margin: 0 0 20px;
  font-size: 15px;
  font-weight: var(--fw-grade-breakdown);
  color: var(--color-gray-dark);
}

/* The date itself — same treatment as .grade-breakdown .breakdown-value:
   black against the line's own muted gray label text. */
.next-assessment-line-date {
  font-weight: var(--fw-breakdown-value);
  color: var(--color-text-primary);
}

.next-assessment-line:empty {
  display: none;
}

/* A column, not the F/S row itself — see .assessment-fs-row below. Final
   Exam (when present) is a child of this too (not a sibling of it in
   .class-card), so it draws from the SAME flex: 1 share of the card
   .assessment-groups already had rather than adding an extra one: the
   card's total height stays whatever it'd naturally be with just
   Formatives/Summatives, and Final Exam's own fixed-size row (see
   .assessment-box--wide) is what shrinks the F/S row to make room for it,
   instead of growing the whole card. */
.assessment-groups {
  position: relative;
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}

.assessment-fs-row {
  display: flex;
  gap: 16px;
  flex: 1;
  min-height: 0;
}

/* Link/unlink toggle for a row whose Formative qualifies for replacement by
   its Summative — sits in the gap between the two boxes, vertically
   aligned (via JS, see positionFormativeLinkButton) with that row. Hidden
   until either box in the pair is hovered (display:flex/none above is
   separately toggled by JS, for scroll visibility — this opacity toggle is
   purely the hover reveal). The button itself is a DOM child of
   .assessment-groups, not .assessment-fs-row (see the comment above
   updateFormativeLinkButton in domain.js) — the hover selector below has to
   target .assessment-groups to actually match it. */
.formative-link-btn {
  position: absolute;
  left: 50%;
  z-index: 5;
  transform: translate(-50%, -50%);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: var(--color-surface);
  /* Floats over the two assessment boxes, same as the dropdowns/calendar —
     so it uses their shadow token, kept (lighter) in dark mode rather than
     dropped like flat cards. */
  box-shadow: 0 2px 6px var(--color-shadow-popover);
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.15s ease;
}

.assessment-groups:hover .formative-link-btn {
  opacity: 1;
  pointer-events: auto;
}

.formative-link-btn:hover {
  background: var(--color-gray-lighter);
}

.formative-link-icon {
  display: block;
  width: 16px;
  height: 16px;
}

.assessment-box {
  position: relative;
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  border: 1.5px solid var(--color-gray-light);
  border-radius: 12px;
  padding: 10px 16px 16px;
}

/* Final Exam, when present, sits below .assessment-fs-row inside
   .assessment-groups (not one of the two side-by-side columns) — sized to
   its own content (always exactly one row), not stretched. .assessment-fs-
   row's own flex: 1 is what does the adjusting: Formatives/Summatives
   shrink to whatever's left of .assessment-groups' fixed share of the
   card after this takes only the height it needs. */
.assessment-box--wide {
  flex: 0 0 auto;
  margin-top: 16px;
  margin-bottom: 8px;
}

.assessment-label {
  font-size: 20px;
  font-weight: var(--fw-assessment-label);
  color: var(--color-text-primary);
}

.score-list {
  list-style: none;
  margin: 6px -12px 0 0;
  padding: 0 4px 0 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  /* Always "thin" (never toggled) so the reserved scrollbar gutter never
     changes width on hover — only its color toggles below, which avoids
     the box reflowing/stretching when the cursor moves over it. */
  scrollbar-width: thin;
  scrollbar-color: transparent transparent;
}

/* Only reveal the scrollbar of the box the cursor is actually over — the
   other box's scrollTop is being synced programmatically, which doesn't
   trigger :hover, so it stays hidden on its own. */
.score-list:hover {
  scrollbar-color: var(--color-scrollbar-thumb) transparent;
}

.score-list::-webkit-scrollbar {
  width: 4px;
}

.score-list::-webkit-scrollbar-track {
  background: transparent;
}

.score-list::-webkit-scrollbar-thumb {
  background: transparent;
  border-radius: 4px;
}

.score-list:hover::-webkit-scrollbar-thumb {
  background: var(--color-scrollbar-thumb);
}

.assessment-box--wide .score-list {
  margin-top: 4px;
}

.score-list li {
  position: relative;
}

.score-list li label {
  display: flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  border-radius: 17px;
  transition: background-color 0.15s ease;
}

/* Hovering the box previews every still-empty row as if selected, light
   gray (filled rows already have their own value and aren't "where you can
   start writing") — but once something inside is actually focused, this
   stops matching entirely (rather than fighting it on specificity), so
   only that one cell's own :focus-within highlight below shows. */
.assessment-box:hover:not(:has(.score-value:focus)) .score-list li:has(.letter-grade--empty) label {
  background: var(--color-gray-lighter);
}

.score-list li label:focus-within {
  background: var(--color-focus-highlight);
}

.score-value {
  font: inherit;
  font-size: 24px;
  color: var(--color-text-primary);
  width: 6ch;
  border: none;
  background: transparent;
  padding: 0;
  margin: 0;
}

.score-value:focus {
  outline: none;
}

/* Only ever populated (via JS) on a Formatives/Summatives box's first row,
   and only while every score in that box is empty — see
   updateEmptyBoxPlaceholder. A separate element rather than a real
   placeholder: a placeholder shares the input's own text box, so shifting
   it left over the icon would also relocate the caret — the moment you
   typed a character, the value would jump to the input's normal position
   while the caret stayed behind. This overlay is purely visual
   (pointer-events: none, absolutely positioned, out of the flex flow) and
   never touches the input itself, so typing always starts at the input's
   normal spot, right of the icon, like any other row. Invisible until the
   box is hovered, matching the row highlight's "preview on hover"
   treatment. 70% of the real score size (24px), so it reads as a hint. */
.score-type-hint {
  position: absolute;
  left: 8px;
  top: 50%;
  transform: translateY(-50%);
  /* 70% of .score-value's 24px — a plain "70%" here would resolve against
     this span's own ancestor chain instead (it's a sibling of .score-value,
     not a descendant), which isn't 24px. */
  font-size: 16.8px;
  color: transparent;
  white-space: nowrap;
  pointer-events: none;
  transition: color 0.15s ease;
}

.assessment-box:hover .score-type-hint {
  color: var(--color-gray-light);
}

/* Clicking the cell hides its hint immediately, even though the mouse is
   still hovering it. Higher specificity than the hover rule above (an
   extra .score-list/li/:has() layer), so it wins regardless of hover. */
.score-list li:has(.score-value:focus) .score-type-hint {
  color: transparent;
}

/* Shows the original typed score next to a Formative row that's currently
   displaying a higher, Summative-replaced value. Empty (and hidden) the
   rest of the time. Positioned absolutely (out of the label's flex flow)
   so it never takes up horizontal space or shifts the icon/input. */
.score-original {
  position: absolute;
  right: 4px;
  top: 50%;
  transform: translateY(-50%);
  transform-origin: left center;
  font-size: 15px;
  color: var(--color-gray-dark);
  pointer-events: none;
}

.score-original:empty {
  display: none;
}

/* A Formative's date takes priority over its replaced-score label — once a
   date is set, the original-score label is hidden entirely (its text is
   still there for when a "change this" control gets added later, just not
   shown). */
.score-list li:has(.score-date-label:not(:empty)) .score-original {
  display: none;
}

/* Hovering the link/unlink button (between the two boxes) previews what
   the Formative would revert to — its original raw score — even for a row
   currently showing a date in that spot instead (force-original is added/
   removed by JS on mouseenter/mouseleave of that button, since the button
   isn't inside this row's own markup for plain :hover to reach). Only
   kicks in when there's actually an original score to show — a row with a
   date but no original score (never replaced by its Summative) keeps
   showing its date. Repeats the :has() clause from the date-priority rule
   above purely to match its specificity, so this override actually wins
   over it. */
.score-list li.force-original:has(.score-original:not(:empty)) .score-original {
  display: inline;
}

.score-list li.force-original:has(.score-original:not(:empty)) .score-date-label {
  display: none;
}

/* Same idea, but for the whole duration of the link/unlink click-triggered
   swap animation (see animateFormativeReveal/animateFormativeUnreveal in
   app.js), not just live hover of that button. Without this, a row with a
   date set would hide its moving/growing number mid-flight the instant the
   cursor left the button (force-original above only reflects live hover,
   so it'd get removed while the animation was still playing) — the
   animated number should only ever revert to hidden-behind-the-date once
   it's actually settled back into being the small, gray label, not while
   it's still the big animating one. Repeats the :has() clause again to
   match/exceed the date-priority rule's specificity. */
.score-list li.swap-animating:has(.score-date-label:not(:empty)) .score-original {
  display: inline;
}

.score-list li.swap-animating .score-date-label {
  display: none;
}

/* Set once a day is picked in the calendar popup (e.g. "Aug 10"). Sits in
   .score-original's exact spot — the two never show at once (see the
   date-takes-priority rule above), so this keeps every row's trailing
   label aligned to the same position whether it's showing a date or an
   original score. Swaps for the date button on hover so it can be
   reopened/changed. */
.score-date-label {
  position: absolute;
  right: 4px;
  top: 50%;
  transform: translateY(-50%);
  font-size: 15px;
  color: var(--color-gray-dark);
  white-space: nowrap;
  pointer-events: none;
}

.score-date-label:empty {
  display: none;
}

/* Hidden on hover (swapped for the button), and also while its calendar is
   open (score-date-btn--active) even after the mouse has moved away — the
   label should never reappear behind an open popup. */
.score-list li:hover .score-date-label,
.score-list li:has(.score-date-btn--active) .score-date-label {
  display: none;
}

/* Hover-only button for a filled score — hidden by default (the date label
   takes its spot once a date is set), revealed on hover so it can be opened
   to set/change the date. Shifts left when the original-score label is
   visible (no date set yet), so it doesn't overlap it. Stays visible
   (score-date-btn--active) while its own calendar is open, even if the
   mouse has moved elsewhere to use the popup. */
.score-date-btn {
  position: absolute;
  right: 14px;
  top: 50%;
  transform: translateY(-50%);
  width: 18px;
  height: 18px;
  padding: 0;
  border: none;
  background: none;
  opacity: 0;
  pointer-events: none;
  cursor: pointer;
  transition: opacity 0.15s ease;
}

.score-date-icon {
  display: block;
  width: 100%;
  height: 100%;
}

.score-list li:hover:has(.letter-grade:not(.letter-grade--empty)) .score-date-btn,
.score-date-btn--active {
  opacity: 1;
  pointer-events: auto;
}

.score-list li:has(.score-original:not(:empty)):has(.score-date-label:empty) .score-date-btn {
  right: 34px;
}

/* --- Score date calendar popup --- */

.score-date-calendar {
  z-index: 50;
  width: 240px;
  background: var(--color-surface);
  border-radius: 14px;
  padding: 12px;
  box-shadow: 0 8px 24px var(--color-shadow-popover);
}

.score-date-calendar-clear-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 10px;
}

.score-date-calendar-clear-label {
  font-size: 13px;
  color: var(--color-gray-dark);
}

.score-date-calendar-clear-btn {
  flex-shrink: 0;
  padding: 5px 12px;
  border: none;
  border-radius: 8px;
  background: var(--color-gray-lighter);
  color: var(--color-gray-dark);
  font: inherit;
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
}

.score-date-calendar-clear-btn:hover {
  background: var(--color-gray-light);
  color: var(--color-text-primary);
}

.score-date-calendar-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 8px;
}

.score-date-calendar-label {
  font-size: 15px;
  font-weight: var(--fw-assessment-label);
  color: var(--color-text-primary);
}

.score-date-nav-btn {
  width: 24px;
  height: 24px;
  border: none;
  border-radius: 50%;
  background: none;
  color: var(--color-text-primary);
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
}

.score-date-nav-btn:hover:not(:disabled) {
  background: var(--color-gray-lighter);
}

.score-date-nav-btn:disabled {
  color: var(--color-gray-light);
  cursor: default;
}

.score-date-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 2px;
}

.score-date-weekday-row {
  margin-bottom: 4px;
}

.score-date-weekday {
  text-align: center;
  font-size: 12px;
  color: var(--color-gray-dark);
}

.score-date-day {
  width: 100%;
  aspect-ratio: 1;
  border: none;
  border-radius: 50%;
  background: none;
  font: inherit;
  font-size: 13px;
  color: var(--color-text-primary);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}

.score-date-day--outside {
  visibility: hidden;
  cursor: default;
}

.score-date-day:hover {
  background: var(--color-gray-lighter);
}

.score-date-day--today {
  font-weight: 700;
  color: var(--color-accent);
}

.score-date-day--selected {
  background: var(--color-accent);
  color: var(--color-surface);
}

.gpa-layout {
  display: flex;
  align-items: flex-start;
  gap: 24px;
}

.gpa-card {
  background: var(--color-surface);
  border-radius: 20px;
  padding: 32px;
  box-shadow: 0 8px 24px var(--color-shadow);
}

.gpa-classes-card {
  width: 320px;
  /* Content width (the card uses the default content-box sizing, so this
     excludes .gpa-card's padding) — a floor so flex-shrink in .gpa-layout
     can never compress it past this. */
  min-width: 300px;
}

/* Matches .class-name's size/weight/color, like the course title on Domain. */
.gpa-classes-title {
  margin: 0 0 4px;
  font-size: 28px;
  font-weight: var(--fw-class-name);
  color: var(--color-text-primary);
}

.gpa-classes-description {
  margin: 0 0 16px;
  font-size: 14px;
  color: var(--color-gray-dark);
  /* Only relevant while it's being collapsed away (see
     hideGpaClassesDescription in gpa.js) — clips the text as its own
     height animates down to 0, rather than letting it overflow. */
  overflow: hidden;
}

.gpa-class-list {
  list-style: none;
  margin: 0 0 16px;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.gpa-class-row {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0;
}

.gpa-class-name {
  font-size: 19px;
  color: var(--color-text-primary);
}

/* Fixed to its badge size (rather than sizing to content, the default)
   since the scroll-driven grade change briefly holds two badges at once
   (the outgoing and incoming icon, both position: absolute — see
   animateGpaBadgeChange in gpa.js) — without an explicit box the button
   would collapse to 0x0 the moment its in-flow content is taken out of
   flow like that. */
.gpa-grade-badge-btn {
  position: relative;
  width: 42px;
  height: 42px;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
  border-radius: 50%;
  line-height: 0;
}

/* Clears the row's grade. Only shown while hovering a row that currently
   has one set (a non-empty data-slug). */
.gpa-clear-btn {
  display: none;
  position: absolute;
  right: 50px;
  top: 50%;
  transform: translateY(-50%);
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border: none;
  background: none;
  color: var(--color-gray-dark);
  font-size: 30px;
  line-height: 1;
  cursor: pointer;
  padding: 0;
}

.gpa-class-row[data-slug]:not([data-slug=""]):hover .gpa-clear-btn {
  display: flex;
}

/* An ungraded class on the GPA list: a hollow ring instead of a filled
   letter icon, so it reads as "not graded yet" rather than a broken image. */
.grade-badge--empty {
  display: inline-block;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  border: 2px solid var(--color-gray-light);
  background: transparent;
  box-sizing: border-box;
}

/* Scoped to the GPA class list only, so this doesn't affect .letter-grade
   badges elsewhere (e.g. the Domain page's course summary icon). */
.gpa-class-row .letter-grade,
.gpa-class-row .grade-badge--empty {
  width: 42px;
  height: 42px;
}

.gpa-paste-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  padding: 10px;
  font: inherit;
  font-size: 15px;
  color: var(--color-gray-dark);
  background: transparent;
  border: 1px solid var(--color-gray-light);
  border-radius: 999px;
  cursor: pointer;
}

.gpa-paste-btn:hover:not(:disabled) {
  background: var(--color-background);
}

.gpa-paste-btn:disabled {
  cursor: default;
}

.paste-spinner {
  display: inline-block;
  width: 14px;
  height: 14px;
  margin-right: 8px;
  border: 2px solid var(--color-gray-light);
  border-top-color: var(--color-gray-dark);
  border-radius: 50%;
  animation: paste-spinner-spin 0.6s linear infinite;
  flex-shrink: 0;
}

@keyframes paste-spinner-spin {
  to {
    transform: rotate(360deg);
  }
}

.gpa-summary-card {
  display: flex;
  flex-direction: column;
}

/* Matches .class-name's size/weight/color, like the course title on Domain. */
.gpa-summary-title {
  margin: 0 0 20px;
  font-size: 28px;
  font-weight: var(--fw-class-name);
  color: var(--color-text-primary);
}

.gpa-summary-values {
  display: flex;
  gap: 48px;
}

/* Single combined score (no AP courses) -> center it instead of leaving it
   pinned to the left like the two-item Unweighted/Weighted layout. */
.gpa-summary-values:has(.gpa-summary-item--combined:not([hidden])) {
  justify-content: center;
}

.gpa-summary-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}

.gpa-summary-item[hidden] {
  display: none;
}

.gpa-summary-item strong {
  font-size: 40px;
  font-weight: 600;
  color: var(--color-text-primary);
  justify-content: center; /* keeps digits centered during the roll animation, when this becomes an inline-flex container */
}

/* Direct-child combinator on purpose — a plain descendant selector here
   would also match every individual digit-slot <span> the roll animation
   builds inside <strong>, forcing each single digit into a 160px box. */
.gpa-summary-item > span {
  min-width: 160px;
  font-size: 17px;
  color: var(--color-gray-dark);
  white-space: nowrap;
  text-align: center;
}

/* The sideways (horizontal) grade picker that opens from a badge. */
.gpa-grade-dropdown {
  position: absolute;
  z-index: 10;
  left: calc(100% + 12px);
  top: 50%;
  transform: translateY(-50%);
  margin: 0;
  padding: 6px;
  list-style: none;
  display: flex;
  align-items: center;
  gap: 4px;
  background: var(--color-surface);
  border-radius: 999px;
  box-shadow: 0 8px 24px var(--color-shadow-popover);
}

.gpa-grade-dropdown:empty {
  display: none;
}

.gpa-grade-option {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
  border-radius: 50%;
  line-height: 0;
}

.gpa-grade-option img {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  object-fit: cover;
  display: block;
}

.gpa-grade-option:hover img {
  outline: 2px solid var(--color-accent);
  outline-offset: 1px;
}

/* Toggles the dropdown between A+..B and B-..F. Flex-centered rather than
   relying on line-height to vertically center the "⋯" glyph — that isn't
   reliable across fonts and left it sitting visibly off-center next to the
   image-filled .gpa-grade-option circles beside it. */
.gpa-grade-more {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: var(--color-background);
  color: var(--color-gray-dark);
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
}

.gpa-grade-more:hover {
  color: var(--color-accent);
}
