/* Inter, self-hosted (SIL OFL 1.1, see fonts/OFL.txt) so no page sends a request to Google Fonts. */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 300 800;
  font-display: swap;
  src: url('fonts/inter-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 300 800;
  font-display: swap;
  src: url('fonts/inter-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* Theme variables are in theme-light.css and theme-dark.css */

.text-muted {
    color: var(--text-muted) !important;
}

.text-main {
    color: var(--text-main) !important;
}

/* Shared gradient-heading treatment. See also .grader-score-val (02_specific.css) which
   applies the same trio directly. Keeping both prefixed and unprefixed background-clip
   here (rather than in per-element inline styles) is what lets iOS/Safari (which still
   requires -webkit-background-clip) and Chrome/Firefox (which accept the unprefixed
   property) both render it. */
.cg-status-success {
    color: var(--success-text);
    margin-top: 0.5rem;
}

.text-gradient {
    background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
}

body {
    margin: 0;
    font-family: 'Inter', sans-serif;
    background-color: var(--bg-color);
    background-image: none;
    color: var(--text-main);
    min-height: 100vh;
    overflow-x: hidden;
    transition: background-color var(--transition-speed), color var(--transition-speed);
}

/* Scrollbar Styling */
::-webkit-scrollbar {
    width: 6px;
}

::-webkit-scrollbar-track {
    background: var(--bg-color);
}

::-webkit-scrollbar-thumb {
    background: var(--glass-border);
    border-radius: 3px;
}

::-webkit-scrollbar-thumb:hover {
    background: var(--accent-primary);
}

/* Layout Containers */
.main-wrapper {
    display: flex;
    min-height: 100vh;
}

.sidebar {
    width: var(--sidebar-width);
    height: 100dvh;
    max-height: 100dvh;
    position: fixed;
    left: 0;
    top: 0;
    background: var(--sidebar-bg);
    backdrop-filter: blur(12px);
    border-right: 1px solid var(--glass-border);
    padding: 0.5rem 0.4rem;
    z-index: 1000;
    transition: width var(--transition-speed) cubic-bezier(0.4, 0, 0.2, 1), transform var(--transition-speed);
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

.nav-container {
    flex: 1;
    overflow-y: auto;
    min-height: 0;
}

.sidebar.collapsed {
    width: var(--sidebar-collapsed-width);
}

.content-area {
    margin-left: var(--sidebar-width);
    flex: 1;
    display: flex;
    flex-direction: column;
    min-height: 100vh;
    min-width: 0; /* Critical for flex child overflow */
    transition: margin-left var(--transition-speed) ease;
    overflow-x: hidden;
}

.sidebar.collapsed ~ .content-area {
    margin-left: var(--sidebar-collapsed-width);
}

.header {
    height: var(--header-height);
    background: var(--glass-bg);
    backdrop-filter: blur(8px);
    border-bottom: 1px solid var(--glass-border);
    display: flex;
    align-items: center;
    justify-content: flex-end;
    padding: 0 1rem;
    position: sticky;
    top: 0;
    z-index: 900;
}

/* Anchor-jump targets (FAQ category navigator, changelog Top links) land clear
   of the sticky header (.header, height: var(--header-height)) instead of
   tucking their first line underneath it. No scroll-margin existed anywhere
   in the app before this. */
#top,
#changelog-top,
[id^="faq-section-"],
[id^="changelog-"] {
    scroll-margin-top: calc(var(--header-height) + 12px);
}

.header-user-text {
    color: inherit;
}

.header-user-link {
    color: var(--text-muted);
    text-decoration: none;
    transition: color var(--transition-speed);
}

.header-user-link:hover {
    color: var(--accent-secondary);
}


.page-container {
    padding: 0.5rem 1rem;
    flex: 1;
}

.footer {
    height: var(--footer-height);
    background: var(--glass-bg);
    border-top: 1px solid var(--glass-border);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0 1rem;
    font-size: 0.75rem;
    color: var(--text-muted);
}

/* Sidebar Elements */
.sidebar-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 0.75rem;
    padding: 0 0.25rem;
}

.sidebar.collapsed .sidebar-header {
    justify-content: center;
}

.sidebar.collapsed .sidebar-title {
    display: none;
}

.nav-link-custom {
    display: flex;
    align-items: center;
    padding: 0.35rem 0.6rem;
    color: var(--text-muted);
    text-decoration: none;
    border-radius: 8px;
    margin-bottom: 0.15rem;
    transition: all 0.2s ease;
    white-space: nowrap;
    overflow: hidden;
}

.sidebar.collapsed .nav-link-custom {
    padding: 0.6rem 0;
    justify-content: center;
}

/* A label wider than the rail ends in an ellipsis instead of a sliced glyph:
   the German labels are close to the rail's width in Inter, and macOS text
   rendering is a few pixels wider than Windows. */
.nav-link-custom span {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}
.sidebar.collapsed .nav-link-custom span {
    display: none;
}

.sidebar.collapsed .nav-link-custom i {
    margin: 0 !important;
    font-size: 1.1rem;
}

.nav-link-custom:hover {
    background: var(--glass-bg);
    color: var(--text-main);
}

.nav-link-custom.active {
    background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
    color: white;
}

.sidebar-footer {
    margin-top: auto;
    margin-left: -0.4rem;
    margin-right: -0.4rem;
    margin-bottom: -0.5rem;
    display: flex;
    flex-direction: column;
    align-items: center;
}
.sidebar-footer > #theme-toggle {
    border-top: 1px solid var(--glass-border);
    width: 100%;
}

.theme-toggle-container {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
    cursor: pointer;
    color: var(--text-muted);
    transition: color 0.2s;
}

.theme-toggle-container:hover {
    color: var(--text-main);
}

.sidebar.collapsed .theme-toggle-text,
.sidebar.collapsed .sidebar-footer span.small {
    display: none;
}
.sidebar.collapsed .sidebar-footer i {
    margin: 0 !important;
}

/* Sidebar Collapse Button */
.collapse-btn {
    background: transparent;
    border: none;
    color: var(--text-muted);
    cursor: pointer;
    padding: 0.25rem;
    border-radius: 4px;
    transition: all 0.2s;
}

.collapse-btn:hover {
    background: var(--glass-bg);
    color: var(--text-main);
}

/* ── Buttons ───────────────────────────────────────────────────────────────
   btn-primary  — flat emerald  — main CTA ("Grade now", "Add student")
   btn-premium  — gradient      — upgrade / premium upsell only
   btn-secondary — outline      — cancel / back / neutral
   btn-danger   — flat red      — destructive, always behind a confirm modal
   btn-xs       — size modifier — pair with any of the above
──────────────────────────────────────────────────────────────────────────── */

.btn-primary {
    background: #10B981;
    border: none;
    color: white;
    padding: 0.3rem 0.8rem;
    border-radius: 6px;
    font-weight: 600;
    font-size: 0.8rem;
    transition: all 0.2s ease;
    cursor: pointer;
}

.btn-primary:hover {
    background: #0ea572;
    transform: translateY(-1px);
}

.btn-premium {
    background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
    border: none;
    color: white;
    padding: 0.3rem 0.8rem;
    border-radius: 6px;
    font-weight: 600;
    font-size: 0.8rem;
    transition: all 0.2s ease;
    cursor: pointer;
}

.btn-premium:hover {
    opacity: 0.85;
    transform: translateY(-1px);
}

.btn-secondary {
    background: transparent;
    border: 1px solid var(--glass-border);
    color: var(--text-main);
    padding: 0.3rem 0.8rem;
    border-radius: 6px;
    font-weight: 600;
    font-size: 0.8rem;
    transition: all 0.2s ease;
    cursor: pointer;
}

.btn-secondary:hover {
    background: rgba(148, 163, 184, 0.25);
    color: var(--text-main);
    border-color: var(--text-muted);
}

.btn-danger {
    background: var(--danger-solid);
    border: none;
    color: white;
    padding: 0.3rem 0.8rem;
    border-radius: 6px;
    font-weight: 600;
    font-size: 0.8rem;
    transition: all 0.2s ease;
    cursor: pointer;
}

.btn-danger:hover {
    background: #b91c1c;
    transform: translateY(-1px);
}

/* ── Badges ────────────────────────────────────────────────────────────────
   Subscription tiers and status indicators.
   All badges: display inline-block, pill shape (border-radius 20px).
──────────────────────────────────────────────────────────────────────────── */

.badge-cg {
    display: inline-block;
    padding: 0.2rem 0.6rem;
    border-radius: 20px;
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.04em;
    line-height: 1;
}

.badge-free {
    background: rgba(16, 185, 129, 0.12);
    color: #065f46;
}

.badge-plus {
    background: rgba(26, 54, 93, 0.1);
    color: #1A365D;
}

.badge-pro {
    background: #1A365D;
    color: #ffffff;
}

.badge-new {
    background: #10B981;
    color: #ffffff;
}

.badge-warn {
    background: #fef3c7;
    color: #92400e;
}

.badge-danger {
    background: #fee2e2;
    color: #991b1b;
}


/* Cards and Tables */
.card-glass {
    background: var(--glass-bg);
    backdrop-filter: blur(12px);
    border: 1px solid var(--glass-border);
    border-radius: 10px;
    padding: 0.75rem 1rem;
    margin-bottom: 0.75rem;
}

/* Plan options (My Account -> Plan & Usage): each add-on flows in a wrapping
   flex row - side by side when there's room, stacked when the screen is narrow -
   instead of a fixed two-column split. Sizing lives here (not inline) so the
   visibility callbacks that rewrite each option's inline style don't clobber it. */
.plan-option-col {
    flex: 1 1 260px;
    min-width: 0;
}

/* My Account tabs: cards flow in a wrapping flex row that fills the available
   width - as many columns as fit, then wrap - instead of a fixed two-column grid
   or a single centered column. The per-card flex sizing lives here (not inline)
   because several cards have their inline style rewritten by visibility callbacks,
   which would otherwise clobber it. align-items: stretch (not the flexbox default
   flex-start) so every card in a wrapped row shares the tallest card's height -
   without it a short card (e.g. What's New) and a tall one (e.g. Plan & Usage)
   visibly differ, which reads as unfinished layout rather than intentional design. */
.account-card-grid {
    display: flex;
    flex-wrap: wrap;
    gap: 1.5rem;
    justify-content: center;
    align-items: stretch;
}
.account-card-grid > .account-tile {
    flex: 1 1 340px;
    max-width: 460px;
    min-width: 0;
}

/* Plan & Usage: the plan-options and connector tiles carry wide content (a
   connector URL in an InputGroup, a two-column option row), so they take a
   row of their own instead of sharing one at the 460px cap above. */
.account-card-grid > .account-tile-wide {
    flex-basis: 100%;
    max-width: 960px;
}

/* Plan & Usage: the plan-options and connector tiles sit side by side as a
   neat two-column row when there's room, stacking below 768px. */
.account-card-grid > .account-tile-half {
    flex-basis: calc(50% - 0.75rem);
    max-width: 480px;
}
@media (max-width: 768px) {
    .account-card-grid > .account-tile-half {
        flex-basis: 100%;
    }
}

h2,
h3,
h4,
h5 {
    margin-top: 0;
}

.mb-4 {
    margin-bottom: 0.75rem !important;
}

.mb-3 {
    margin-bottom: 0.5rem !important;
}

.mb-2 {
    margin-bottom: 0.25rem !important;
}

/* Global Compact Form Elements */
.form-control:not(textarea),
.form-input-custom,
.dbc-input,
.sidebar-dropdown-custom .Select-control {
    height: 30px !important;
    padding: 0.2rem 0.5rem !important;
    font-size: 1rem !important;
    border-radius: 6px !important;
    background: var(--glass-bg) !important;
    border: 1px solid var(--glass-border) !important;
    color: var(--text-main) !important;
}

/* A To-Do item's text is a real input so it can be corrected in place, but the
   list should still read as a list rather than a column of form fields - so it
   carries no border or background until the teacher actually focuses it. The
   .form-control rule above wins on specificity, hence the !important here. */
.todo-title-input {
    background: transparent !important;
    border-color: transparent !important;
    padding-left: 0.25rem !important;
}
.todo-title-input:hover {
    border-color: var(--glass-border) !important;
}
.todo-title-input:focus {
    background: var(--glass-bg) !important;
    border-color: var(--accent-secondary) !important;
    box-shadow: none !important;
}

/* Global placeholder readability — dark & light mode */
::placeholder {
    color: var(--text-muted) !important;
    opacity: 1 !important; /* Firefox reduces opacity by default */
}

::-webkit-input-placeholder {
    color: var(--text-muted) !important;
}

::-moz-placeholder {
    color: var(--text-muted) !important;
    opacity: 1 !important;
}

:-ms-input-placeholder {
    color: var(--text-muted) !important;
}

/* Global Dash Dropdown Compactness */
.Select-control {
    height: 30px !important;
    min-height: 30px !important;
    background: var(--glass-bg) !important;
    border: 1px solid var(--glass-border) !important;
    font-size: 1rem !important;
    color: var(--text-main) !important;
}

.Select-placeholder {
    line-height: 28px !important;
    color: var(--text-muted) !important;
}

.Select-value {
    line-height: 28px !important;
    color: var(--text-main) !important;
}

/* Premium Disabled Select Dropdown Styling */
.Select.is-disabled {
    opacity: 0.65 !important;
}

.Select.is-disabled .Select-control {
    background: rgba(0, 0, 0, 0.02) !important;
    border-color: rgba(0, 0, 0, 0.05) !important;
}

.Select.is-disabled .Select-placeholder {
    color: var(--text-muted) !important;
}

.Select--single>.Select-control .Select-value {
    padding-left: 8px !important;
}

.has-value.Select--single>.Select-control .Select-value .Select-value-label, 
.has-value.is-pseudo-focused.Select--single>.Select-control .Select-value .Select-value-label {
    color: var(--text-main) !important;
}

.Select-input {
    height: 28px !important;
    font-size: 1rem !important;
}

.Select-menu-outer {
    background-color: var(--bg-color) !important;
    border: 1px solid var(--glass-border) !important;
    color: var(--text-main) !important;
    z-index: 9999 !important;
}

.Select.is-open {
    z-index: 10050 !important;
}

/* Elevate parent containers when a dropdown is open to resolve backdrop-filter stacking context layering bugs.
   backdrop-filter also becomes the containing block for the menu's position:fixed coordinates, which pushes the
   open menu away from the control (see ticket #590/#589) - neutralize it here too while the menu is open. */
.card-glass:has(.Select.is-open),
.col:has(.Select.is-open),
[class*="col-"]:has(.Select.is-open),
.table-dark-custom tbody tr:has(.Select.is-open) {
    z-index: 10060 !important;
    position: relative !important;
    backdrop-filter: none !important;
}

.Select-option {
    background-color: var(--bg-color) !important;
    color: var(--text-main) !important;
}

.Select-option.is-focused,
.Select-option.is-selected {
    background-color: var(--accent-primary) !important;
    color: white !important;
}

/* Native <select> (dbc.Select) option popups are drawn by the browser and
   inherit no app theme, so in dark mode white option text (from --text-main)
   landed on the browser's default white popup and was unreadable. Theme the
   <option>s explicitly. Chrome/Edge/Firefox honor these on the native popup;
   macOS/Safari draw an OS-themed popup and ignore them harmlessly. This only
   matches real native <option> elements, so it cannot affect dcc.Dropdown. */
select option {
    background-color: var(--bg-color) !important;
    color: var(--text-main) !important;
}
/* ...which also overrode the browser's grey for a disabled option (the AI Lab's
   models without an API key), so it read as available (ticket #689). */
select option:disabled {
    color: var(--text-muted) !important;
}

/* dcc.Dropdown (Dash 4) closed control: Dash defaults its box to a SOLID WHITE
   background with dark text, driven by its own tokens that ignore our theme, so
   in dark mode the box stayed white and the value text was unreadable. Same idea
   as the <select> options above -- give the base box a themed background so the
   dark page shows through, and let the text follow var(--text-main). Uses theme
   vars, so it is correct in both light and dark. */
.dash-dropdown {
    background-color: var(--glass-bg) !important;
    color: var(--text-main) !important;
}
.dash-dropdown-trigger-icon {
    color: var(--text-main) !important;
}

/* Dash 4 rewrote dcc.Dropdown with new `.dash-dropdown-*` class names, which
   broke all the react-select theming above. Mirror it here so menus keep a
   solid themed background and stay layered above tables/cards. */
/* Closed control (the box showing the selected value before the menu opens).
   Without this, .dash-dropdown-value below forces light theme text with no
   matching background, making the selected label invisible on light/white
   ancestors (e.g. a table header cell). */
.dash-dropdown-trigger {
    background: var(--glass-bg) !important;
    border: 1px solid var(--glass-border) !important;
    min-height: 30px !important;
}
.dash-dropdown-content {
    background-color: var(--bg-color) !important;
    border: 1px solid var(--glass-border) !important;
    color: var(--text-main) !important;
    z-index: 10050 !important;
}
.dash-dropdown-option {
    background-color: var(--bg-color) !important;
    color: var(--text-main) !important;
}
.dash-dropdown-option:hover,
.dash-dropdown-option.active,
.dash-dropdown-option.selected {
    background-color: var(--accent-primary) !important;
    color: #fff !important;
}
/* A disabled option (`"disabled": True`, e.g. a model whose provider has no API
   key) is a <label> holding an <input disabled>. Dash greys it itself, but the
   !important text colour and hover fill above override that, so it looked like
   every other option and just ignored the click (ticket #689). */
.dash-dropdown-option:has(input[disabled]),
.dash-dropdown-option:has(input[disabled]):hover,
.dash-dropdown-option.active:has(input[disabled]) {
    background-color: var(--bg-color) !important;
    color: var(--text-muted) !important;
    opacity: 0.55;
    cursor: not-allowed;
}
.dash-dropdown-actions,
.dash-dropdown-action-button,
.dash-dropdown-search {
    color: var(--text-main) !important;
}
/* The search box needs a BACKGROUND as well as a colour, and it takes TWO
   rules because the white comes from two places (ticket #641: white-on-white
   search text in dark mode, unreadable the moment the teacher starts typing).

   Dash's own stylesheet puts the fill on the wrapper -
   `.dash-dropdown-search-container { background: var(--Dash-Fill-Inverse-Strong) }`
   - and the <input> itself has no background rule at all, so it falls back to
   the browser's default white field. Theming only one of them leaves the other
   one white. Both are therefore cleared here: the container takes the same
   --bg-color/--glass-border treatment as .dash-dropdown-content above so it
   reads as a search field inside the menu, and the input goes transparent so
   the container shows through rather than carrying a third colour that could
   drift from the other two.

   This has to target .dash-dropdown-search*, NOT react-select's
   `.Select-input > input`: Dash 4 rewrote dcc.Dropdown and that markup no
   longer exists (async-dropdown.js contains no Select-* class at all, and
   nothing else in this app renders react-select since dash_table is unused),
   which is why the first attempt at this fix was a no-op. */
.dash-dropdown-search-container {
    background: transparent !important;
    border-color: var(--glass-border) !important;
}
.dash-dropdown-search {
    background: transparent !important;
}
/* Trigger text: DROPDOWN_STYLE forces a dark inline color; keep it readable on
   the dark theme (mirrors the old .Select-value override). */
.dash-dropdown-value,
.dash-dropdown-value-item {
    color: var(--text-main) !important;
}
.dash-dropdown-placeholder {
    color: var(--text-muted) !important;
}
/* Elevate the container when a dropdown menu is open (backdrop-filter stacking
   fix), mirroring the .Select.is-open rule above. `.dash-dropdown-content` is
   only in the DOM while the menu is open. backdrop-filter also becomes the
   containing block for the menu's position:fixed coordinates, mispositioning
   it relative to the container instead of the viewport (ticket #590/#589) -
   drop it here too while the menu is open. */
.card-glass:has(.dash-dropdown-content),
.col:has(.dash-dropdown-content),
[class*="col-"]:has(.dash-dropdown-content),
.table-dark-custom tbody tr:has(.dash-dropdown-content),
.modal-content:has(.dash-dropdown-content) {
    z-index: 10060 !important;
    position: relative !important;
    backdrop-filter: none !important;
}

/* Table Compact Styling */
.table-dark-custom {
    --bs-table-bg: transparent;
    --bs-table-color: var(--text-main);
    color: var(--text-main) !important;
    background: transparent !important;
    border-collapse: separate;
    border-spacing: 0;
}

.table-dark-custom thead th {
    --bs-table-bg: transparent;
    background: rgba(255, 255, 255, 0.03);
    border-bottom: 1px solid var(--glass-border) !important;
    color: var(--text-muted) !important;
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    padding: 6px 10px !important;
}

.table-dark-custom tbody td {
    --bs-table-bg: transparent;
    color: var(--text-main) !important;
    padding: 4px 10px !important;
    vertical-align: middle;
    border-bottom: 1px solid var(--glass-border);
    font-size: 0.85rem;
    overflow: visible !important;
}

.table-dark-custom tbody tr {
    position: relative;
    z-index: 1;
}

.table-dark-custom tbody tr:hover {
    z-index: 2;
    background: rgba(255, 255, 255, 0.02) !important;
}

.table-dark-custom tbody tr:focus-within {
    z-index: 99;
    background: rgba(255, 255, 255, 0.02) !important;
}

/* The teacher's optional per-student colour, shown in the student list as a dot
   in front of the name (utils/ui_helpers.py::_highlight_dot) rather than as the
   full-row tint the gradebook uses - the list is a working surface where a
   tinted row would compete with the checkboxes, badges and action buttons. The
   colour itself is set inline per student, so only the shape lives here. */
.student-highlight-dot {
    display: inline-block;
    width: 9px;
    height: 9px;
    border-radius: 50%;
    margin-right: 7px;
    vertical-align: middle;
    border: 1px solid var(--glass-border);
}

/* Compact Dropdown in Table */
.table-dark-custom .Select-control {
    height: 28px !important;
    min-height: 28px !important;
    background: var(--glass-bg) !important;
    border: 1px solid var(--glass-border) !important;
    font-size: 0.8rem;
    color: var(--text-main) !important;
}

.table-dark-custom .Select-placeholder,
.table-dark-custom .Select-value {
    line-height: 26px !important;
    color: var(--text-main) !important;
}

.table-dark-custom .Select--single>.Select-control .Select-value {
    padding-left: 8px !important;
}

.btn-xs {
    padding: 0.15rem 0.4rem;
    font-size: 0.75rem;
    border-radius: 4px;
}

/* Responsiveness */
@media (max-width: 768px) {
    .sidebar {
        transform: translateX(-100%);
        width: 260px !important;
        /* Solid surface, exactly as .calendar-session-drawer needs one: on
           desktop the sidebar is glass beside the page, but here it slides
           OVER it, and --sidebar-bg is only 80% opaque - so whatever sits
           behind it shows through. Saturated things show through worst, which
           is why the calendar's amber Record and violet Lesson plan chips read
           as coloured smudges across the open menu. The blur goes with it: it
           has nothing left to blur once the panel is opaque, and it costs a
           compositing layer to do nothing. */
        background: var(--bg-color);
        backdrop-filter: none;
    }

    .sidebar.open {
        transform: translateX(0);
    }

    /* On mobile, the desktop 'collapsed' state is irrelevant —
       only .open controls visibility. Without this fix, a collapsed
       desktop sidebar would stay hidden even after tapping the hamburger. */
    .sidebar.collapsed {
        transform: translateX(-100%);
    }

    .sidebar.open.collapsed {
        transform: translateX(0);
        width: 260px !important;
    }

    .content-area {
        margin-left: 0 !important;
    }

    .sidebar.collapsed ~ .content-area {
        margin-left: 0 !important;
    }

    /* Backdrop overlay — tap outside to close sidebar */
    .sidebar-backdrop {
        display: none;
        position: fixed;
        inset: 0;
        background: rgba(0, 0, 0, 0.45);
        z-index: 999;
        backdrop-filter: blur(2px);
        animation: fadeInBackdrop 0.2s ease;
    }

    .sidebar-backdrop.active {
        display: block;
    }

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

    .stack.flex-row {
        flex-wrap: wrap !important;
    }
}

/* AI Diff View Styling */
ins {
    color: var(--bs-success) !important;
    font-weight: bold !important;
    text-decoration: none !important;
}

del {
    color: var(--bs-danger) !important;
    font-weight: bold !important;
    text-decoration: line-through !important;
}

/* ---------------------------------------------------------------------------
   Rich-text note box (pages/components/note_editor.py)
   Shared by lesson notes, day notes and the home page's notes widget, so the
   four surfaces cannot drift apart visually.
   --------------------------------------------------------------------------- */
.note-editor {
    display: flex;
    flex-direction: column;
}

.note-toolbar {
    display: flex;
    gap: 2px;
    margin-bottom: 4px;
}

.note-tool-btn {
    background: none;
    border: 1px solid transparent;
    border-radius: 5px;
    color: var(--text-muted);
    cursor: pointer;
    font-size: 0.75rem;
    line-height: 1;
    padding: 4px 8px;
}
.note-tool-btn:hover {
    background: rgba(255, 255, 255, 0.08);
    border-color: var(--glass-border);
    color: var(--text-main);
}

.note-toolbar-compact .note-tool-btn {
    font-size: 0.7rem;
    padding: 2px 5px;
}

.note-textarea {
    background: rgba(255, 255, 255, 0.02) !important;
    border: 1px solid var(--glass-border) !important;
    border-radius: 6px !important;
    color: var(--text-main) !important;
    font-size: 0.8rem !important;
    padding: 8px !important;
    resize: vertical;
}
.note-textarea:focus {
    background: var(--glass-bg) !important;
    border-color: var(--accent-secondary) !important;
}

/* The note at rest. It has to LOOK like the field it is: a transparent border
   revealed only on hover left it reading as plain paragraph text, with nothing
   to suggest it could be edited (and nothing at all to aim at on a touch
   screen, where there is no hover). It therefore carries the same border,
   radius, background and box metrics as .note-textarea - which also means
   clicking it swaps in the editor without the layout jumping. */
.note-display {
    background: rgba(255, 255, 255, 0.02);
    border: 1px solid var(--glass-border);
    border-radius: 6px;
    color: var(--text-main);
    cursor: text;
    font-size: 0.8rem;
    padding: 8px;
    transition: background 0.12s ease, border-color 0.12s ease;
}
.note-display:hover {
    background: var(--glass-bg);
    border-color: var(--accent-secondary);
}
.note-display > *:last-child {
    margin-bottom: 0;
}
.note-display ul,
.note-display ol {
    margin-bottom: 0.35rem;
    padding-left: 1.1rem;
}
/* Opt-in: a box that takes its share of its container instead of a fixed height
   (pages/components/note_editor.py fill=True; the gradebook's "Fill this column" window).
   Both the rendered note and the textarea use the full height, so toggling between them
   does not make the box jump. */
.note-editor--fill {
    flex: 1 1 0;
    min-height: var(--gb-note-min-height, 140px);
}
.note-editor--fill .note-display {
    flex: 1 1 auto;
    overflow: auto;
}
.note-editor--fill .note-edit {
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    min-height: 0;
}
.note-editor--fill .note-textarea {
    flex: 1 1 auto;
    height: 100% !important;
    resize: none;
}

.note-display-empty {
    color: var(--text-muted);
    font-style: italic;
}

/* Links inside rendered notes are the whole point of the feature: they must
   read as links against the midnight background, in every note surface. */
.note-display a,
.note-rendered a {
    color: var(--accent-secondary);
    text-decoration: underline;
    overflow-wrap: anywhere;
}
.note-display a:hover,
.note-rendered a:hover {
    color: var(--accent-primary);
}

/* --- App-wide loading feedback -----------------------------------------
   Two pieces, both deliberately outside the Dash component tree (appended
   to document.body by assets/11_app_busy.js) so neither costs dash-renderer
   anything on the per-store-update TreeContainer walk described in CLAUDE.md.

   1) .cg-busy-bar: a slim indeterminate bar shown once ANY _dash-update-
      component request has been in flight for 400ms. The delay is what
      keeps a fast callback (a keystroke, a checkbox) from flashing it.
   2) .cg-busy-pill: added on top once the same request has run past 2.5s,
      to reassure a teacher on a slow connection that the app is still
      working rather than stuck.

   z-index 10070 sits above the sidebar (1000), header (900) and every
   modal (10050/10060) - a lagging save triggered from inside a modal is
   exactly a case where this needs to be visible. */
.cg-busy-bar {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 3px;
    z-index: 10070;
    pointer-events: none;
    overflow: hidden;
    background: transparent;
}
.cg-busy-bar::after {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    height: 100%;
    width: 40%;
    background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
    animation: cg-busy-bar-slide 1.1s ease-in-out infinite;
}
@keyframes cg-busy-bar-slide {
    0% { transform: translateX(-100%); }
    100% { transform: translateX(350%); }
}

.cg-busy-pill {
    position: fixed;
    bottom: 24px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 10070;
    pointer-events: none;
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 10px 18px;
    border-radius: 999px;
    background: var(--glass-bg, rgba(15, 23, 42, 0.9));
    border: 1px solid var(--glass-border);
    backdrop-filter: blur(8px);
    color: var(--text-main);
    font-size: 0.85rem;
    font-weight: 600;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
    opacity: 0;
    animation: cg-busy-pill-fade-in 0.25s ease-out forwards;
}
@keyframes cg-busy-pill-fade-in {
    to { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
    .cg-busy-bar::after {
        animation: none;
        width: 100%;
        opacity: 0.6;
    }
    .cg-busy-pill {
        animation: none;
        opacity: 1;
    }
}

/* Generic placeholder shown inside a page's main content container while its
   on-load callback is still running - the app-wide version of the pattern
   introduced for the gradebook grid (see .cg-loading-block usage in
   pages/gradebook_results.py and utils/layout_helpers.py::loading_placeholder).
   Deliberately understated: it occupies the same space the real content will
   fill, so nothing jumps when that content lands, and it reuses
   .cg-inline-spinner from 02_specific.css rather than a second spinner
   treatment. min-height is a floor, not a fixed height, so short content
   still shrinks to fit afterwards. */
.cg-loading-block {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 180px;
    padding: 24px;
    color: var(--text-muted);
    font-size: 0.9rem;
}

/* Small caps heading above one group of items - the "What's New" panel files a
   release's highlights under areas (system & speed / look & layout / everyday
   improvements) so a teacher can see at a glance which part of the app a line
   is about. Its own class rather than the gradebook's .gb-legend-label: same
   look, but the changelog must not depend on a gradebook stylesheet. */
.cg-area-label {
    font-size: 0.68rem;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-main);
    margin-bottom: 2px;
}

/* Breathing room above each area block so the groups read as separate blocks.
   The space belongs on the GROUP, not on the label: the label is always the
   first child of its own group, so :first-child on it would match every heading
   and cancel the margin everywhere. The first group sits right under the entry
   title, which already carries its own margin. */
.cg-area-group {
    margin-top: 14px;
}

.cg-area-group:first-child {
    margin-top: 0;
}

/* Where a finished checkout is reported (app.py's #checkout-result-alert,
   pages/callbacks/app_checkout_callbacks.py). Fixed in the shell, not inline on
   a page, because checkout opens from the plan picker, the paywall and My
   School alike; above modals, since the paywall is one. */
.cg-toast-slot {
    position: fixed;
    top: 72px;
    right: 16px;
    z-index: 1090;
    max-width: min(420px, calc(100vw - 32px));
}
.cg-toast-slot:empty {
    display: none;
}
