/* Palette shared with MyNotes (see its web/static/render/note.css, which owns the
   canonical variables there). The five names MyNotes exposes — --fg, --muted,
   --primary, --link, --danger — carry the same values here, so an event
   description rendered by the MyNotes render kit inside <NotePanel> sits on the
   same colours as the rest of this UI. The remaining names are MyCal's own,
   derived from the same Tailwind grey/blue ramp rather than the old neutral
   greys, so the two apps read as one product.

   --text is kept as an alias of --fg because ~200 rules already reference it. */
:root {
    /* Tells the browser which palette to draw native widgets (form controls,
       scrollbars, date pickers) from; without it a dark theme still gets
       light-mode controls. */
    color-scheme: light;

    /* MyNotes' canonical five */
    --fg: #1f2937;
    --muted: #6b7280;
    --primary: #2563eb;
    --link: #2563eb;
    --danger: #dc2626;

    --primary-hover: #1d4ed8;
    /* Tint of --primary for drop targets and other wash-over-content fills. */
    --primary-soft: rgba(37, 99, 235, 0.12);

    --bg: #f3f4f6;
    --surface: #ffffff;
    --surface-alt: #f9fafb;
    --border: #e5e7eb;
    --border-light: #f3f4f6;
    --text: var(--fg);
    --text-muted: #4b5563;
    --text-subtle: #6b7280;
    --text-faint: #9ca3af;
    --text-disabled: #9ca3af;
    --text-placeholder: #d1d5db;
    --hover-bg: #f3f4f6;
    --hover-bg-light: #f9fafb;
    --accent-hover-bg: #eff6ff;
    --week-hover: #eff6ff;
    --week-num-hover: #dbeafe;
    --error-bg: #fee2e2;
    --error-color: var(--danger);
    --success-bg: #dcfce7;
    --success-color: #16a34a;
    --tag-bg: #dbeafe;
    --tag-color: #1d4ed8;
    --feed-disabled-bg: #fee2e2;
    --feed-disabled-color: var(--danger);

    /* The two sidebar-footer buttons resolve their resting text and hover fill
       through these rather than straight off --text-subtle and --hover-bg, which
       is what the MySuite spec's role table names and what the other two apps
       use directly. The indirection exists for one theme only: since the footer
       stopped painting --surface (see .sidebar-footer), the controls sit on --bg,
       and in MyCal's LIGHT palette --bg and --hover-bg are the same #f3f4f6.
       That collision is the one §5.4/§10.1 of the shared spec already names, and
       against --bg it breaks both colours at once — the label at 4.393:1 fails
       WCAG 1.4.3 (4.5:1 for 12.8px/400), and the fill paints #f3f4f6 over
       #f3f4f6 at 1.000:1, so the fill disappears. Degradation rather than loss:
       the hover rule moves three properties, and the label and border colours
       still change when the fill does not.

       Dark has no collision and is not diverged: both aliases below resolve to
       the spec's own tokens there, and every dark figure improved when the
       backdrop moved (label 5.782 → 6.987, fill 1.424 → 1.721). Only light
       differs, which is why these are aliases per theme and not new colours. */
    --sidebar-footer-text: var(--text-muted);       /* #4b5563 → 6.867:1 on --bg */
    --sidebar-footer-hover-bg: var(--border);       /* #e5e7eb → 1.125:1 on --bg */
}

/* Layout tokens, kept out of the palette block above so that one stays purely
   about colour — it is the block the MyNotes palette is synced against, and the
   one the "never hardcode a hex outside :root" rule points at. Nothing here is
   theme-dependent, so there is no [data-theme="dark"] counterpart. */
:root {
    /* The left column, and .brand above it — one column, so one number. It does
       not bound the footer's two buttons: .sidebar-footer reclaims .app's
       horizontal padding, so their budget is this width again after its own 8px
       insets — 200px for a 174px pair, 26px spare, which absorbs a system-ui
       about a quarter wider than this machine's before the row overflows.

       In rem, not px, and 12.5rem is exactly 200px at the default 16px root: the
       buttons down there are sized in rem too, so a px column would keep its
       width while they grew and push Settings out over the calendar. That starts
       at a 20px root — Chrome's "Large" — which is a WCAG 1.4.4 failure reachable
       from the browser's own font menu. Keep this in rem. */
    --sidebar-width: 12.5rem;

    /* .app's horizontal padding — the gap between the window and everything in
       the app. A token rather than a literal because .sidebar-footer cancels it
       with a negative margin to reach the window's edge, and the two must stay
       equal and opposite. Redefined at the narrow breakpoint, which the footer
       then follows automatically. */
    --app-padding-x: 16px;
}

[data-theme="dark"] {
    color-scheme: dark;

    --fg: #f3f4f6;
    --muted: #9ca3af;
    --primary: #3b82f6;
    --link: #60a5fa;
    --danger: #ef4444;

    --primary-hover: #60a5fa;
    --primary-soft: rgba(59, 130, 246, 0.18);

    --bg: #111827;
    --surface: #1f2937;
    --surface-alt: #273549;
    --border: #374151;
    --border-light: #2b3644;
    --text-muted: #d1d5db;
    --text-subtle: #9ca3af;
    --text-faint: #6b7280;
    --text-disabled: #6b7280;
    --text-placeholder: #4b5563;
    --hover-bg: #374151;
    --hover-bg-light: #273549;
    --accent-hover-bg: #1e3a5f;
    --week-hover: #1c2a44;
    --week-num-hover: #1e3a5f;
    --error-bg: #3f1d1d;
    --success-bg: #14321f;
    --success-color: #22c55e;
    --tag-bg: #1e3a5f;
    --tag-color: #93c5fd;
    --feed-disabled-bg: #3f1d1d;

    /* Both resolve to the shared spec's own tokens: dark is not diverged. They
       are restated here only because the light block aliases them elsewhere —
       without these two lines the footer would inherit light's indirection into
       the dark theme, where --text-muted is #d1d5db (a resting label at 12.039:1,
       near-white, reading as permanently hovered next to the #f3f4f6 hover text)
       and --border is… also #374151. See the light block for why the indirection
       exists at all.

       DO NOT COLLAPSE THESE TWO BLOCKS INTO ONE UNSCOPED PAIR. It is tempting for
       the fill, because dark's --border and --hover-bg are the same #374151, so
       `var(--border)` alone would give the right colour in both themes today.
       That equality is a coincidence in the palette, not a rule: the day --border
       moves in dark, the fill would follow it away from the mandated --hover-bg
       silently and in the one theme that is not supposed to be diverging at all.
       Naming the token each theme actually owes its value to is the point. */
    --sidebar-footer-text: var(--text-subtle);      /* #9ca3af → 6.987:1 on --bg */
    --sidebar-footer-hover-bg: var(--hover-bg);     /* #374151 → 1.721:1 on --bg */
}

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
    background: var(--bg);
    color: var(--text);
    line-height: 1.5;
}

/* Lucide icons are always the whole content of an icon-only button (the
   buttons that carry a label are text alone). Centring them keeps the SVG's
   baseline from leaving a descender gap under the glyph. */
.lucide {
    vertical-align: middle;
    flex-shrink: 0;
}

.app {
    margin: 0;
    padding: 8px var(--app-padding-x);
    position: relative;
    /* A flex column at least as tall as the window, so .app-layout's `flex: 1`
       has something to fill and .left-sidebar always reaches the bottom of the
       viewport. Without it the column stops at the height of whatever view sits
       beside it, and the footer — pushed down by `margin-top: auto` — lands
       wherever the month or year grid happens to end: measured 96px above the
       window in month and 170px in year, both varying with window size. The
       footer's two buttons have to sit the same distance from the window's
       bottom edge in every view, or MyCal moves against itself when the user
       switches view. `min-height`, not `height`, so a grid taller than the
       window still grows the page and scrolls the way it always has; the three
       views that fill the viewport override this with `height` below. */
    display: flex;
    flex-direction: column;
    min-height: 100dvh;
}

.app.drag-over::after {
    content: 'Drop .ics file to import';
    position: fixed;
    inset: 0;
    background: var(--primary-soft);
    border: 3px dashed var(--primary);
    border-radius: 8px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.3rem;
    color: var(--primary);
    font-weight: 500;
    z-index: 999;
    pointer-events: none;
}

/* Top bar */
.top-bar {
    display: flex;
    align-items: center;
    margin-bottom: 16px;
    /* The bar must still grow to fit a wrapped heading — do not turn this into
       a fixed `height`. What it no longer decides is where the *badge* sits:
       that offset is authored on .brand and .brand-logo below.

       It is still load-bearing for everything else in the row. Removing it lets
       the bar collapse to 34px, and the nav rides up with it: `nav h1` measured
       at y = 11.188 with this line and 8.188 without, in Year view at 1280. The
       badge stays at 14 either way, so no test here catches its removal — which
       is exactly why this says so rather than reading as a historical note.

       The history, because it is the evidence that 14 was a chosen number and
       not a coincidence. This said `min-height: 40px` put the
       badge's top edge at 8px (.app padding) + 6px = 14px, "the same offset
       MyMail's sidebar header gives its own badge via padding-top: 14px". The
       *intent* was right and is now the shared rule
       (../mysuite/spec/app-logo.md §4). The *mechanism* was not: centring
       .brand in this bar makes the badge's position a remainder of the date
       heading's line count, and min-height is a floor a two-line heading simply
       exceeds. Measured before the fix, landing view, 16px root: y = 14 at
       1920, 27.609 at 1280, 44.406 at 1024. */
    min-height: 40px;
}

/* Brand mark + label, sized to match the left sidebar so the nav lines up with
   .app-main. Shares --sidebar-width with .left-sidebar because the two are one
   column, and e2e/tests/calendar-views.spec.ts pins .brand-reload-btn's right
   edge to the sidebar's. */
.brand {
    width: var(--sidebar-width);
    margin-right: 12px;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 1.1rem;
    font-weight: 600;
    color: var(--text);
    /* The badge's distance from the top of the window is a contract value
       (../mysuite/spec/app-logo.md §4), so it is authored here rather than
       left to fall out of the bar's height. `flex-start` takes this block out
       of .top-bar's centring, and 6px on top of .app's 8px padding-top is the
       14px the badge has to sit at — whatever the heading does beside it.
       MyMail reaches the same 14 the same way: a fixed inset on its
       .sidebar-header, not a remainder.

       The label and Reload move with it deliberately. They are one block with
       the mark, and pinning the mark while leaving them centred would pull the
       row apart every time the heading wraps. Reload's *right* edge is what
       calendar-views.spec.ts pins, and that is horizontal — unaffected.

       Overridden in the ≤600px block, where .top-bar becomes a column and this
       would mean something else entirely. */
    align-self: flex-start;
    margin-top: 6px;
}

/* The app mark (components/Logo.tsx, the favicon glyph) on a --primary square.
   Box, radius and glyph size are MyMail's .logo-icon verbatim, so the badge
   before "MyCal" here and the one before "MyMail" there are the same size. The
   glyph carries no width/height of its own, so both are set here. */
.brand-logo {
    width: 28px;
    height: 28px;
    flex-shrink: 0;
    /* Out of .brand's own `align-items: center`, so the badge's offset is two
       px declarations end to end — .app's 8px padding-top plus .brand's 6px —
       and nothing else. Centring here would leave a remainder of
       (.brand height − 28) / 2, which is zero today only because the badge is
       the tallest thing in the row, and only by 1.61px: giving .brand-name a
       taller line-height, or padding the Reload button, would then move the
       mark. Measured at 19.344px and 18px respectively, at a 16px root.

       Deleting this line IS caught now, and that is a change from what this
       comment used to say: calendar-views.spec.ts re-measures the badge at 20px
       and 24px roots, where the rem-sized label overtakes the badge and the
       remainder stops being zero — verified by deleting it, y = 16.5 at 20px.
       At a 16px root alone it still measures 14 either way, so the root loop is
       the whole of the coverage.

       What those two levers DO still move, today and unguarded here, is the
       *label* beside it — .brand-name does not opt out of the centring the way
       this rule does. That is a recorded, deliberately unclosed drift path
       (../mysuite/spec/app-name-label.md §4.2, §4.3); web/AGENTS.md carries the
       operands, and the label's own y assertion is what fires.

       The shared contract requires the badge's offset to be *declared* rather
       than computed (../mysuite/spec/app-logo.md §4.2), and this is the
       declaration that makes that true at every root font size. */
    align-self: flex-start;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--primary);
    color: #fff;
    border-radius: 6px;
}

.brand-logo svg {
    width: 17px;
    height: 17px;
}

.brand-name {
    /* overflow zeroes the automatic minimum size, so the label ellipsizes
       instead of forcing .brand past its 200px. */
    min-width: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Reload, at the far end of the brand block — the corner MyMail reloads from,
   `margin-left: auto` past the label and all (see its .sidebar-reload-btn). And
   since .brand is the sidebar's 200px, the button lands on the right edge of the
   column below it. A ghost button, the way MyMail draws this one: no outline of
   its own, unlike the framed pair in .sidebar-footer. */
.brand-reload-btn {
    margin-left: auto;
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: none;
    border: none;
    border-radius: 6px;
    cursor: pointer;
    padding: 4px;
    line-height: 1;
    color: var(--text-subtle);
}

.brand-reload-btn:hover {
    background: var(--hover-bg);
    color: var(--text);
}

/* Navigation */
.nav {
    display: flex;
    align-items: center;
    gap: 8px;
    min-width: 0;
}

.nav h1 {
    font-size: 1.4rem;
    font-weight: 500;
    margin-left: 8px;
}

.nav button {
    display: inline-flex;
    align-items: center;
    padding: 6px 12px;
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text);
    border-radius: 6px;
    cursor: pointer;
    font-size: 0.9rem;
    font-family: inherit;
}

.nav button:hover {
    background: var(--hover-bg);
}

/* Calendar */
.calendar-header {
    display: grid;
    grid-template-columns: 28px repeat(7, minmax(0, 1fr));
    text-align: center;
    font-weight: 600;
    font-size: 0.85rem;
    color: var(--text-subtle);
    margin-bottom: 4px;
}

.week-number-header {
    /* empty spacer above week numbers */
}

.weekday {
    padding: 8px 0;
}

.calendar-grid {
    display: grid;
    grid-template-columns: 28px repeat(7, minmax(0, 1fr));
    gap: 1px;
    background: var(--border);
    border: 1px solid var(--border);
    border-radius: 6px;
    overflow: hidden;
}

.week-number {
    background: var(--surface-alt);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 0.7rem;
    color: var(--text-disabled);
    cursor: pointer;
}

.week-number:hover {
    background: var(--week-num-hover);
    color: var(--primary);
}

.day {
    background: var(--surface);
    min-height: 100px;
    min-width: 0;
    padding: 4px;
    cursor: pointer;
    overflow: hidden;
}

.day:hover {
    background: var(--hover-bg-light);
}

.day.other-month {
    background: var(--surface-alt);
    color: var(--text-placeholder);
}

.day.today .day-number {
    background: var(--primary);
    color: #fff;
    border-radius: 50%;
    width: 28px;
    height: 28px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

.day-number {
    font-size: 0.85rem;
    display: inline-block;
    margin-bottom: 2px;
}

.day-events {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.event-chip {
    font-size: 0.75rem;
    padding: 2px 4px;
    border-radius: 4px;
    background: var(--primary);
    color: #fff;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    cursor: pointer;
}

.event-chip:hover {
    opacity: 0.85;
}

/* Dialog */
/* The `* { margin: 0 }` reset above cancels the UA's `margin: auto`, so the
   dialog is top-anchored; overriding the UA `max-height: calc(100% - 38px)`
   lets it use the full viewport height before it starts scrolling. */
.event-dialog {
    border: none;
    border-radius: 8px;
    padding: 24px;
    width: 90%;
    max-width: 560px;
    max-height: 100dvh;
    box-shadow: 0 8px 32px rgba(0,0,0,0.2);
    background: var(--surface);
    color: var(--text);
}

.event-dialog::backdrop {
    background: rgba(0,0,0,0.4);
}

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

.dialog-header h2 {
    font-size: 1.2rem;
    font-weight: 500;
}

.close-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: none;
    border: none;
    border-radius: 6px;
    cursor: pointer;
    color: var(--text-subtle);
    padding: 4px;
}

.close-btn:hover {
    background: var(--hover-bg);
    color: var(--text);
}

.event-dialog label {
    display: block;
    margin-bottom: 5px;
    font-size: 0.85rem;
    color: var(--text-muted);
}

.event-dialog input,
.event-dialog textarea {
    display: block;
    width: 100%;
    margin-top: 4px;
    padding: 8px;
    border: 1px solid var(--border);
    border-radius: 6px;
    font-size: 0.95rem;
    font-family: inherit;
    background: var(--surface);
    color: var(--text);
}

.event-dialog input:disabled,
.event-dialog textarea:disabled {
    background: var(--bg);
    color: var(--text);
}

.color-picker {
    margin-bottom: 8px;
    font-size: 0.85rem;
    color: var(--text-muted);
}

.color-options {
    display: flex;
    gap: 6px;
    margin-top: 4px;
    flex-wrap: wrap;
}

.color-swatch {
    width: 28px;
    height: 28px;
    border-radius: 50%;
    cursor: pointer;
    border: 3px solid transparent;
    padding: 0;
    appearance: none;
}

.color-swatch.selected {
    border-color: var(--text);
}

.color-swatch:hover {
    opacity: 0.8;
}

.dialog-actions {
    display: flex;
    gap: 8px;
    align-items: center;
    flex-wrap: wrap;
}

/* The share row's Send/Cancel share this styling so they read as the same kind
   of button as the dialog header's Edit/Copy/Share/Delete/Close. */
.dialog-actions button,
.share-input-row button {
    padding: 7px 14px;
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text);
    border-radius: 6px;
    cursor: pointer;
    font-size: 0.9rem;
    font-family: inherit;
}

.dialog-actions button:hover:not(:disabled),
.share-input-row button:hover:not(:disabled) {
    background: var(--hover-bg);
}

.dialog-actions button[type="submit"] {
    background: var(--primary);
    color: #fff;
    border-color: var(--primary);
}

.dialog-actions button[type="submit"]:hover:not(:disabled) {
    background: var(--primary-hover);
    border-color: var(--primary-hover);
}

.dialog-actions button.danger {
    color: var(--danger);
    border-color: var(--danger);
}

.dialog-actions button.danger:hover:not(:disabled) {
    background: var(--error-bg);
}

.dialog-actions button:disabled,
.share-input-row button:disabled {
    opacity: 0.6;
    cursor: default;
}

.error {
    background: var(--error-bg);
    color: var(--error-color);
    padding: 8px 12px;
    border-radius: 6px;
    margin-bottom: 12px;
    font-size: 0.85rem;
}

/* Compact ghost icon button for the top-bar actions — the same shape MyNotes
   uses for its `.btn-icon`, so the two apps' toolbars match. */
.settings-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: none;
    border: 1px solid var(--border);
    border-radius: 6px;
    cursor: pointer;
    padding: 6px 9px;
    color: var(--text-subtle);
}

.settings-btn:hover {
    background: var(--hover-bg);
    color: var(--text);
}

.settings-dialog {
    border: none;
    border-radius: 8px;
    padding: 24px;
    width: 90%;
    max-width: 420px;
    box-shadow: 0 8px 32px rgba(0,0,0,0.2);
    background: var(--surface);
    color: var(--text);
}

.settings-dialog::backdrop {
    background: rgba(0,0,0,0.4);
}

.settings-dialog label {
    display: block;
    margin-bottom: 12px;
    font-size: 0.85rem;
    color: var(--text-muted);
}

.settings-dialog input[type="text"],
.settings-dialog input[type="url"] {
    display: block;
    width: 100%;
    margin-top: 4px;
    padding: 8px;
    border: 1px solid var(--border);
    border-radius: 6px;
    font-size: 0.95rem;
    font-family: inherit;
    background: var(--surface);
    color: var(--text);
}

.settings-dialog select {
    display: block;
    width: 100%;
    margin-top: 4px;
    padding: 8px;
    border: 1px solid var(--border);
    border-radius: 6px;
    font-size: 0.95rem;
    font-family: inherit;
    background: var(--surface);
    color: var(--text);
}

/* Top bar actions */
.top-bar-actions {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-left: auto;
    /* give up width before the nav does when the top bar gets tight */
    flex-shrink: 3;
    min-width: 0;
}

/* Import dialog */
.import-tabs {
    display: flex;
    gap: 4px;
    margin-bottom: 16px;
}

.import-tab {
    padding: 6px 16px;
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text);
    border-radius: 6px;
    cursor: pointer;
    font-size: 0.9rem;
    font-family: inherit;
}

.import-tab:hover:not(.active) {
    background: var(--hover-bg);
}

.import-tab.active {
    background: var(--primary);
    color: #fff;
    border-color: var(--primary);
}

.import-success {
    background: var(--success-bg);
    color: var(--success-color);
    padding: 8px 12px;
    border-radius: 6px;
    margin-bottom: 12px;
    font-size: 0.85rem;
}

.import-hint {
    font-size: 0.8rem;
    color: var(--text-faint);
    margin-bottom: 8px;
}

/* Toast notifications */
.toast-container {
    position: fixed;
    bottom: 24px;
    right: 24px;
    display: flex;
    flex-direction: column;
    gap: 8px;
    z-index: 1000;
    max-width: 360px;
    pointer-events: none;
}

/* Inverted surface, as in MyNotes: --fg on --bg, so the toast flips with the
   theme instead of being a fixed near-black slab. */
.toast-item {
    display: flex;
    align-items: center;
    gap: 10px;
    background: var(--fg);
    color: var(--bg);
    padding: 12px 16px;
    border-radius: 6px;
    font-size: 0.9rem;
    box-shadow: 0 4px 12px rgba(0,0,0,0.25);
    pointer-events: all;
    animation: toast-in 0.25s ease-out;
}

.toast-error {
    background: var(--danger);
    color: #fff;
}

.toast-persistent {
    border-left: 3px solid var(--danger);
}

@keyframes toast-in {
    from { opacity: 0; transform: translateY(16px); }
    to { opacity: 1; transform: translateY(0); }
}

.toast-message { flex: 1; line-height: 1.4; }

.toast-btns {
    display: flex;
    align-items: center;
    gap: 6px;
    flex-shrink: 0;
}

.toast-retry-btn {
    padding: 3px 9px;
    background: var(--primary);
    color: #fff;
    border: none;
    border-radius: 6px;
    font-size: 12px;
    font-weight: 600;
    cursor: pointer;
}
.toast-retry-btn:hover { background: var(--primary-hover); }

/* The toast paints its own inverted surface, so these follow the toast's text
   colour rather than the page's. */
.toast-close-btn {
    display: inline-flex;
    align-items: center;
    background: none;
    border: none;
    color: inherit;
    opacity: 0.65;
    cursor: pointer;
    line-height: 1;
    padding: 2px 4px;
}
.toast-close-btn:hover { opacity: 1; }

/* View toggle */
.view-toggle {
    display: flex;
    gap: 0;
    margin-left: 4px;
}

.view-toggle button {
    padding: 6px 12px;
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text);
    cursor: pointer;
    font-size: 0.85rem;
    font-family: inherit;
}

.view-toggle button:first-child {
    border-radius: 6px 0 0 6px;
}

.view-toggle button + button {
    border-left: none;
}

.view-toggle button:last-child {
    border-radius: 0 6px 6px 0;
}

.view-toggle button.active {
    background: var(--primary);
    color: #fff;
    border-color: var(--primary);
}

.view-toggle button:hover:not(.active) {
    background: var(--hover-bg);
}

/* Checkbox label in form */
.checkbox-label {
    display: flex !important;
    flex-direction: row !important;
    align-items: center;
    gap: 8px;
    margin-bottom: 5px;
    font-size: 0.85rem;
    color: var(--text-muted);
    cursor: pointer;
}

.checkbox-label input[type="checkbox"] {
    width: auto;
    margin: 0;
}

/* Week view */
.week-view {
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--surface);
    display: flex;
    flex-direction: column;
    /* Fills the flex column .app becomes for this view; .week-body scrolls. */
    flex: 1;
    min-height: 0;
}

.week-header {
    display: grid;
    grid-template-columns: 60px repeat(7, minmax(0, 1fr));
    border-bottom: 1px solid var(--border);
    background: var(--surface-alt);
}

.time-gutter-header {
    border-right: 1px solid var(--border);
    text-align: center;
    padding: 8px 4px;
    font-size: 0.85rem;
}

.week-day-header {
    text-align: center;
    padding: 8px 4px;
    font-size: 0.85rem;
    border-right: 1px solid var(--border-light);
    cursor: pointer;
}

.week-day-header:last-child {
    border-right: none;
}

.week-day-header.today .week-day-number {
    background: var(--primary);
    color: #fff;
    border-radius: 50%;
    width: 28px;
    height: 28px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

.week-day-name {
    display: block;
    font-weight: 600;
    color: var(--text-subtle);
    font-size: 0.75rem;
    text-transform: uppercase;
}

.week-day-number {
    display: inline-block;
    margin-top: 2px;
    font-size: 0.95rem;
}

.week-body {
    position: relative;
    overflow-y: auto;
    flex: 1;
    min-height: 0;
}

.week-grid {
    display: grid;
    grid-template-columns: 60px repeat(7, minmax(0, 1fr));
}

.time-gutter {
    font-size: 0.7rem;
    color: var(--text-disabled);
    text-align: right;
    padding: 2px 8px 0 0;
    height: 48px;
    border-right: 1px solid var(--border);
    border-bottom: 1px solid var(--border-light);
}

.hour-cell {
    height: 48px;
    border-bottom: 1px solid var(--border-light);
    border-right: 1px solid var(--border-light);
    cursor: pointer;
}

.hour-cell--hover-full {
    background: var(--week-hover);
}

.hour-cell--hover-top-half {
    background: linear-gradient(to bottom, var(--week-hover) 50%, transparent 50%);
}

.hour-cell--hover-bottom-half {
    background: linear-gradient(to bottom, transparent 50%, var(--week-hover) 50%);
}

.week-events-overlay {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    display: grid;
    grid-template-columns: 60px repeat(7, minmax(0, 1fr));
    pointer-events: none;
}

.week-events-gutter-spacer {
    pointer-events: none;
}

.week-day-events {
    position: relative;
    pointer-events: none;
}

.week-event {
    position: absolute;
    border-radius: 4px;
    padding: 2px 4px;
    color: #fff;
    font-size: 0.7rem;
    overflow: hidden;
    cursor: pointer;
    pointer-events: auto;
    z-index: 1;
    border: 1px solid rgba(255, 255, 255, 0.5);
    box-sizing: border-box;
}

.week-event {
    cursor: grab;
}

.week-event:hover {
    opacity: 0.85;
}

.week-event.dragging {
    box-shadow: 0 4px 12px rgba(0,0,0,0.3);
    cursor: grabbing;
    z-index: 10;
}

.resize-handle {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    height: 6px;
    cursor: ns-resize;
    border-radius: 0 0 4px 4px;
    opacity: 0;
}

.week-event:hover .resize-handle {
    opacity: 1;
    background: rgba(255,255,255,0.3);
}

.week-event.short-event {
    display: flex;
    align-items: center;
    gap: 4px;
    white-space: nowrap;
}

.week-event.short-event .week-event-title {
    display: inline;
    overflow: hidden;
    text-overflow: ellipsis;
}

.week-event.short-event .week-event-time {
    flex-shrink: 0;
}

.week-event-title {
    font-weight: 500;
    display: block;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.week-event-time {
    font-size: 0.65rem;
    opacity: 0.9;
}

/* All-day row in week view */
.week-allday-row {
    display: grid;
    grid-template-columns: 60px repeat(7, minmax(0, 1fr));
    border-bottom: 1px solid var(--border);
    min-height: 28px;
    flex-shrink: 0;
}

.allday-label {
    font-size: 0.7rem;
    color: var(--text-disabled);
    text-align: right;
    padding: 4px 8px 4px 0;
    border-right: 1px solid var(--border);
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    justify-content: flex-start;
}

.allday-toggle {
    cursor: pointer;
    font-size: 0.6rem;
    color: var(--primary);
    margin-top: 2px;
}

.allday-toggle:hover {
    color: var(--primary-hover);
}

.allday-more {
    font-size: 0.6rem;
    color: var(--text-subtle);
    cursor: pointer;
    padding: 0 4px;
}

.allday-more:hover {
    color: var(--primary);
}

.allday-cell {
    padding: 2px;
    border-right: 1px solid var(--border-light);
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
    overflow-x: hidden;
}

.allday-cell:last-child {
    border-right: none;
}

.allday-event {
    font-size: 0.7rem;
    padding: 2px 4px;
    border-radius: 4px;
    background: var(--primary);
    color: #fff;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    cursor: grab;
    position: relative;
}

.allday-event:hover {
    opacity: 0.85;
}

.allday-event.dragging {
    box-shadow: 0 2px 8px rgba(0,0,0,0.3);
    cursor: grabbing;
    z-index: 10;
}

.allday-cell:has(.drag-preview) {
    overflow: visible;
}

/* Day view */
.day-view {
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--surface);
    display: flex;
    flex-direction: column;
    /* Fills the flex column .app becomes for this view; .day-view-body scrolls. */
    flex: 1;
    min-height: 0;
}

.day-view-header {
    display: grid;
    grid-template-columns: 60px 1fr;
    border-bottom: 1px solid var(--border);
    background: var(--surface-alt);
}

.day-view-day-header {
    grid-column: 2;
    text-align: center;
    padding: 8px 4px;
    font-size: 0.85rem;
    cursor: pointer;
}

.day-view-day-header.today .day-view-day-number {
    background: var(--primary);
    color: #fff;
    border-radius: 50%;
    width: 28px;
    height: 28px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

.day-view-day-name {
    display: block;
    font-weight: 600;
    color: var(--text-subtle);
    font-size: 0.75rem;
    text-transform: uppercase;
}

.day-view-day-number {
    display: inline-block;
    margin-top: 2px;
    font-size: 0.95rem;
}

.day-view-allday-row {
    display: grid;
    grid-template-columns: 60px 1fr;
    border-bottom: 1px solid var(--border);
    min-height: 28px;
    flex-shrink: 0;
}

.day-view-allday-cell {
    padding: 2px;
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
    overflow-x: hidden;
}

.day-view-body {
    position: relative;
    overflow-y: auto;
    flex: 1;
    min-height: 0;
}

.day-view-grid {
    display: grid;
    grid-template-columns: 60px 1fr;
}

.day-view-events-overlay {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    display: grid;
    grid-template-columns: 60px 1fr;
    pointer-events: none;
}

.day-view-events-gutter-spacer {
    pointer-events: none;
}

.day-view-day-events {
    position: relative;
    pointer-events: none;
}

/* Year view */
.year-view {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 12px;
}

.year-month {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 6px;
    overflow: hidden;
}

.year-month-header {
    padding: 5px 8px;
    font-weight: 600;
    font-size: 0.8rem;
    background: var(--surface-alt);
    border-bottom: 1px solid var(--border-light);
    cursor: pointer;
}

.year-month-header:hover {
    background: var(--accent-hover-bg);
    color: var(--link);
}

.year-month-grid {
    padding: 2px 4px 3px;
}

.year-weekday-row {
    display: grid;
    grid-template-columns: 16px repeat(7, 1fr);
    text-align: center;
    font-size: 0.55rem;
    color: var(--text-disabled);
    font-weight: 600;
}

.year-weekday {
    padding: 1px 0;
    line-height: 1.8;
}

.year-week-row {
    display: grid;
    grid-template-columns: 16px repeat(7, 1fr);
    text-align: center;
    align-items: center;
}

.year-week-number {
    font-size: 0.5rem;
    color: var(--text-placeholder);
    cursor: pointer;
    border-radius: 2px;
}

.year-week-number:hover {
    background: var(--accent-hover-bg);
    color: var(--link);
}

.year-day {
    font-size: 0.65rem;
    line-height: 2.2;
    border-radius: 50%;
    text-align: center;
    position: relative;
}

.year-day-other {
    color: var(--text-placeholder);
}

.year-day-today {
    background: var(--primary);
    color: #fff;
    font-weight: 600;
    width: 24px;
    height: 24px;
    line-height: 18px;
    border-radius: 50%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin: auto;
}

.year-day-has-events:not(.year-day-today) {
    font-weight: 700;
    color: var(--link);
}

.year-day-has-events:not(.year-day-today)::after {
    content: '';
    display: block;
    width: 3px;
    height: 3px;
    border-radius: 50%;
    background: var(--primary);
    position: absolute;
    bottom: -1px;
    left: 50%;
    transform: translateX(-50%);
}

/* The views that scroll internally (schedule, week, day) turn the app into a
   flex column so they fill the remaining space exactly, down to .app's bottom
   padding. Sizing them with a max-height offset instead would have to guess the
   height of the top bar, which differs per view (the day view's title wraps to
   a second row) and per breakpoint, and any slack left over shows up as dead
   space under the view. A fourth view that scrolls its own body belongs in the
   selector list of all four rules below, not just this one. */
.app:has(.schedule-view, .week-view, .day-view) {
    /* The flex column comes from .app's base rule; only the height changes here,
       from at-least-the-window to exactly-the-window, which is what stops the
       page scrolling and hands the scrolling to the view instead. */
    height: 100dvh;
    overflow: hidden;
}

.app:has(.schedule-view, .week-view, .day-view) .app-main {
    display: flex;
    flex-direction: column;
    min-height: 0;
    overflow: hidden;
}

/* .app is the viewport here and does not scroll, so a left column taller than
   it — enough calendars under the mini month — would be clipped with nowhere to
   scroll. Give .sidebar-content its own scrollport and let it take the free
   space; .sidebar-footer is its sibling, not its child, so it lands on the
   bottom of the column either way. Note this makes .sidebar-content a
   scrollport horizontally too: anything put in it has to fit the column *minus
   the scrollbar gutter* — about 185px of the 200px wherever classic scrollbars
   are drawn — or clip itself, the way .calendar-sidebar-name ellipsizes.
   Keeping the footer outside this element is exactly so its two buttons never
   pay that gutter: the pair has 26px of slack, so a ~15px gutter would eat most
   of it and leave the row a hair from overflowing on a slightly wider system-ui.
   (This said "only 10px to spare" for a while. That number predated the footer's
   negative margin, which hands the column's width back to the buttons after
   their own insets — a stale budget quoted as a live justification.) */
.app:has(.schedule-view, .week-view, .day-view) .sidebar-content {
    flex: 1;
    overflow-y: auto;
}

/* That scrollport ends at .app's bottom padding, which is the gap the sticky
   inset below is there to keep. Keeping the inset as well would double it and
   lift the two buttons above the bottom edge of the view beside them. */
.app:has(.schedule-view, .week-view, .day-view) .sidebar-footer {
    bottom: 0;
}

.schedule-view {
    width: 100%;
    overflow-y: auto;
    flex: 1;
    min-height: 0;
}

.schedule-empty {
    padding: 24px 16px;
    text-align: center;
    color: var(--text-faint);
    font-size: 0.9rem;
}

.schedule-date-header {
    position: sticky;
    top: 0;
    background: var(--bg);
    padding: 10px 16px;
    font-weight: 600;
    font-size: 0.9rem;
    color: var(--text);
    cursor: pointer;
    z-index: 1;
}

.schedule-date-header:hover {
    background: var(--accent-hover-bg);
    color: var(--link);
}

.schedule-event {
    margin: 4px 8px;
    padding: 8px 12px;
    border-radius: 6px;
    color: #fff;
    cursor: pointer;
}

.schedule-event:hover {
    opacity: 0.85;
}

.schedule-event.past-event {
    opacity: 0.5;
}

.schedule-event-time {
    font-size: 0.75rem;
    opacity: 0.9;
    margin-top: 2px;
}

.schedule-event-title {
    font-size: 0.95rem;
    font-weight: 500;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.schedule-event-location {
    font-size: 0.8rem;
    opacity: 0.85;
    margin-top: 2px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.schedule-sentinel {
    height: 1px;
}

/* Search */
.search-input {
    padding: 6px 12px;
    border: 1px solid var(--border);
    border-radius: 6px;
    font-size: 0.9rem;
    font-family: inherit;
    width: 200px;
    min-width: 110px;
    background: var(--surface);
    color: var(--text);
}

/* The one remaining `outline: none` in this file, and it fails the bar the
   sidebar footer's buttons now meet: a border tint is not a 2px perimeter, and
   under forced colours the border colour is overridden so nothing is left at
   all. Deliberately not fixed here — it is part of an app-wide focus pass that
   was deferred rather than folded into the footer work, and doing it properly
   means the same clip check the footer got, since this sits in a tight top bar.
   Until then this is a known gap, not an oversight. */
.search-input:focus {
    outline: none;
    border-color: var(--primary);
}

.search-results {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 6px;
    overflow: hidden;
}

.search-results-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 8px 16px;
    background: var(--bg);
    border-bottom: 1px solid var(--border);
    font-size: 0.85rem;
    color: var(--text-subtle);
}

.search-clear-btn {
    display: inline-flex;
    align-items: center;
    background: none;
    border: none;
    cursor: pointer;
    color: var(--text-faint);
    padding: 4px;
    border-radius: 6px;
}

.search-clear-btn:hover {
    background: var(--hover-bg);
    color: var(--text);
}

.search-result-item {
    padding: 12px 16px;
    border-bottom: 1px solid var(--border-light);
    cursor: pointer;
}

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

.search-result-item:hover {
    background: var(--hover-bg-light);
}

.search-result-item.search-result-past {
    opacity: 0.55;
}

.search-result-title {
    font-weight: 500;
    font-size: 0.95rem;
    margin-bottom: 2px;
}

.search-result-date {
    font-size: 0.8rem;
    color: var(--text-subtle);
}

.search-result-time {
    font-size: 0.8rem;
    color: var(--text-faint);
}

.search-result-desc {
    font-size: 0.8rem;
    color: var(--text-faint);
    margin-top: 4px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.search-empty {
    padding: 24px 16px;
    text-align: center;
    color: var(--text-faint);
    font-size: 0.9rem;
}

/* Responsive */
@media (max-width: 600px) {
    /* Narrower gutter here; .sidebar-footer's negative margin reads the same
       token, so it keeps reaching the window edge without a second rule. */
    :root { --app-padding-x: 8px; }
    .app { padding: 4px var(--app-padding-x); }
    .day { min-height: 60px; }
    .event-chip { font-size: 0.65rem; }
    .week-header { grid-template-columns: 40px repeat(7, minmax(0, 1fr)); }
    .week-grid { grid-template-columns: 40px repeat(7, minmax(0, 1fr)); }
    .week-events-overlay { grid-template-columns: 40px repeat(7, minmax(0, 1fr)); }
    .week-allday-row { grid-template-columns: 40px repeat(7, minmax(0, 1fr)); }
    .time-gutter { font-size: 0.6rem; padding-right: 4px; }
    .day-view-header { grid-template-columns: 40px 1fr; }
    .day-view-grid { grid-template-columns: 40px 1fr; }
    .day-view-events-overlay { grid-template-columns: 40px 1fr; }
    .day-view-allday-row { grid-template-columns: 40px 1fr; }
    .week-day-name { font-size: 0.65rem; }
    .year-view { grid-template-columns: repeat(2, 1fr); gap: 8px; }

    /* Compact toolbar */
    .top-bar {
        flex-direction: column;
        align-items: stretch;
        gap: 6px;
        margin-bottom: 8px;
    }
    /* No room for the mark and the label, but Reload rides in this block and
       still has to be reachable — keep the row and drop the two that are only
       identification here, leaving the button at the top right of the bar. */
    .brand {
        width: auto;
        margin-right: 0;
        /* .top-bar is a column here, so its cross axis is horizontal:
           `align-self: flex-start` would shrink this row to its content width
           rather than align it vertically, stranding Reload in the middle of
           the bar. The badge is hidden at this width anyway (§9.3), so the 14px
           rule has nothing to hold on to — hand the row back to `stretch`. */
        align-self: auto;
        margin-top: 0;
    }
    .brand-logo,
    .brand-name {
        display: none;
    }
    .nav {
        flex-wrap: wrap;
        gap: 4px;
    }
    .nav button {
        padding: 4px 10px;
        font-size: 0.8rem;
    }
    .nav h1 {
        font-size: 1.1rem;
        margin-left: 4px;
        flex: 1;
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
    .view-toggle {
        order: 1;
        width: 100%;
        margin-left: 0;
        margin-top: 2px;
    }
    .view-toggle button {
        flex: 1;
        font-size: 0.75rem;
        padding: 5px 4px;
        text-align: center;
    }
    .top-bar-actions {
        gap: 4px;
        margin-left: 0;
    }
    .search-input {
        flex: 1;
        min-width: 0;
        width: auto;
        font-size: 0.8rem;
        padding: 4px 8px;
    }
    .settings-btn {
        padding: 5px 7px;
    }
    /* No room for the sidebar's calendars, but the footer's two buttons still
       have to be reachable — the column collapses to just the footer, stacked
       below the main content. The footer keeps its negative margin-left here,
       so the buttons stay 8px from the window's left edge. The footer itself is
       full-bleed on both sides here — measured 0→375 at a 375px viewport —
       because the rule below cancels .app's padding on the right as well. What
       matters for the contract is the buttons' 8px on the left, which is the
       edge the eye and the other two apps line up on.

       (This paragraph claimed "8px on the left, 16px on the right, and that
       asymmetry is deliberate" until the measurement above was taken. It was
       describing the layout as it would be *without* the margin-right below,
       which has been there the whole time.)

       The *vertical* half of that rule does not survive this breakpoint: .app's
       padding drops to 4px, so the buttons sit 4px from the window's bottom
       rather than 8px, and the focus outline's 4px reaches the window edge
       exactly. Still within the spec's 4px floor, but with none to spare. The
       narrow layout is explicitly out of scope for the 8px bottom rule — if
       that ever changes, this padding is what has to move. */
    .app-layout {
        flex-direction: column;
    }
    /* `.app-layout` qualifies these only to out-specify the base rules, which
       sit later in the file — at equal specificity source order wins and this
       whole block was silently doing nothing but the `display: none` below.
       That was harmless while the footer painted --bg with no right border. It
       stopped being harmless when it started painting --surface: the column
       stayed 200px, so the footer rendered as a white stub two-thirds across a
       phone screen with its right border hanging in mid-air. */
    .app-layout .left-sidebar {
        width: auto;
        order: 1;
    }
    .left-sidebar > :not(.sidebar-footer) {
        display: none;
    }
    .app-layout .sidebar-footer {
        position: static;
        margin-top: 0;
        /* Full-bleed on both sides here, so the band ends on the window rather
           than in mid-air. There is no border-right to cancel any more — the
           base rule's went with the fill in 8719695, so `border-right: none`
           lived on here as a no-op overriding nothing. Removed rather than kept
           "in case": a cancellation with nothing to cancel reads as evidence
           that the thing it names is still there. */
        margin-right: calc(-1 * var(--app-padding-x));
    }
}

/* Read-only event detail view */
.event-title-display {
    font-size: 1.1rem;
    font-weight: 600;
    margin: 0 0 10px;
    color: var(--text);
}

.detail-row {
    display: flex;
    align-items: baseline;
    font-size: 0.95rem;
    margin-bottom: 6px;
    color: var(--text);
}

.detail-row-block {
    align-items: flex-start;
    flex-direction: column;
    gap: 4px;
}

.detail-label {
    font-size: 0.8rem;
    color: var(--text-muted);
    min-width: 72px;
    flex-shrink: 0;
}

/* Description display */
.description-display {
    margin-top: 4px;
    padding: 8px;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 6px;
    font-size: 0.95rem;
    line-height: 1.5;
    overflow-wrap: break-word;
}

.description-display p {
    margin-bottom: 0.5em;
}

.description-display p:last-child {
    margin-bottom: 0;
}

.description-display ul, .description-display ol {
    margin-left: 1.5em;
    margin-bottom: 0.5em;
}

.description-display a {
    color: var(--primary);
}

/* Rich text editor (Quill) overrides */
.event-dialog .ql-toolbar {
    border-radius: 6px 6px 0 0;
    border-color: var(--border);
    font-family: inherit;
    background: var(--surface);
}

.event-dialog .ql-container {
    border-radius: 0 0 6px 6px;
    border-color: var(--border);
    font-family: inherit;
    font-size: 0.95rem;
    min-height: 80px;
    /* Kept short so a long description does not push the note picker below the fold */
    max-height: 95px;
    overflow-y: auto;
    background: var(--surface);
    color: var(--text);
}

.event-dialog .ql-editor {
    min-height: 80px;
}

/* Quill dark mode icon overrides */
[data-theme="dark"] .ql-toolbar .ql-stroke {
    stroke: var(--text-subtle);
}
[data-theme="dark"] .ql-toolbar .ql-fill {
    fill: var(--text-subtle);
}
[data-theme="dark"] .ql-toolbar .ql-picker {
    color: var(--text-subtle);
}
[data-theme="dark"] .ql-toolbar .ql-picker-options {
    background: var(--surface);
    border-color: var(--border);
}
[data-theme="dark"] .ql-toolbar button:hover .ql-stroke,
[data-theme="dark"] .ql-toolbar button.ql-active .ql-stroke {
    stroke: var(--primary);
}
[data-theme="dark"] .ql-toolbar button:hover .ql-fill,
[data-theme="dark"] .ql-toolbar button.ql-active .ql-fill {
    fill: var(--primary);
}

.form-row {
    display: flex;
    gap: 12px;
}
.form-row > label {
    flex: 1;
    min-width: 0;
}
.event-dialog select {
    display: block;
    width: 100%;
    margin-top: 4px;
    padding: 8px;
    border: 1px solid var(--border);
    border-radius: 6px;
    font-size: 0.95rem;
    font-family: inherit;
    background: var(--surface);
    color: var(--text);
}

.coord-row {
    display: flex;
    gap: 12px;
}
.coord-row label {
    flex: 1;
}

.interval-row {
    display: flex;
    align-items: center;
    gap: 8px;
}

.byday-picker {
    margin: 4px 0 8px;
}
.byday-picker > span {
    display: block;
    font-weight: 500;
    margin-bottom: 4px;
    font-size: 0.9rem;
}
.byday-buttons {
    display: flex;
    gap: 4px;
    flex-wrap: wrap;
}
.byday-btn {
    padding: 4px 8px;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--surface);
    color: var(--text);
    cursor: pointer;
    font-size: 0.8rem;
    font-family: inherit;
    min-width: 36px;
    text-align: center;
}
.byday-btn.active {
    background: var(--primary);
    color: #fff;
    border-color: var(--primary);
}
.byday-btn:hover {
    border-color: var(--primary);
}

.monthly-options {
    margin: 4px 0 8px;
}
.radio-label {
    display: flex;
    align-items: center;
    gap: 6px;
    margin: 4px 0;
    font-size: 0.9rem;
    cursor: pointer;
}

.exdates-section, .rdates-section {
    margin: 4px 0 8px;
}
.exdates-section > span, .rdates-section > span {
    display: block;
    font-weight: 500;
    margin-bottom: 4px;
    font-size: 0.9rem;
}
.exdates-list, .rdates-list {
    display: flex;
    flex-direction: column;
    gap: 4px;
}
.exdate-item, .rdate-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 4px 8px;
    background: var(--bg);
    border-radius: 6px;
    font-size: 0.85rem;
}
.rdate-add-row {
    display: flex;
    gap: 8px;
    align-items: center;
    margin-bottom: 4px;
}
.rdate-add-row input[type="date"] {
    flex: 1;
}
.small-btn {
    padding: 3px 8px;
    font-size: 0.8rem;
    font-family: inherit;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--surface);
    color: var(--text);
    cursor: pointer;
}
.small-btn:hover {
    background: var(--hover-bg);
}
.small-btn.danger {
    color: var(--danger);
    border-color: var(--danger);
}
.small-btn.danger:hover:not(:disabled) {
    background: var(--error-bg);
}

.small-btn:disabled {
    background: var(--border);
    color: var(--text-disabled);
    border-color: var(--border);
    cursor: not-allowed;
}

/* A Lucide glyph spun in place — used on the feed refresh button while its
   request is in flight. */
.is-spinning {
    animation: spin 1s linear infinite;
}

/* Import spinner */
.spinner {
    display: inline-block;
    width: 14px;
    height: 14px;
    border: 2px solid rgba(255,255,255,0.4);
    border-top-color: #fff;
    border-radius: 50%;
    animation: spin 0.6s linear infinite;
    vertical-align: middle;
    margin-right: 6px;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* Highlight event from search */
.highlight-event {
    animation: event-highlight 2s ease-out;
}
@keyframes event-highlight {
    0%, 20% { box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 70%, transparent); }
    100% { box-shadow: 0 0 0 3px transparent; }
}

/* Past events */
.event-chip.past-event,
.week-event.past-event,
.allday-event.past-event { opacity: 0.5; }

/* Duration row */
.duration-row {
    display: flex;
    align-items: center;
    gap: 6px;
}

/* Time input with timezone label */
.time-input-row {
    display: flex;
    align-items: center;
    gap: 6px;
}

.tz-label {
    font-size: 0.85em;
    color: var(--text-faint);
    white-space: nowrap;
}

/* Categories display */
.categories-display {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    padding: 4px 0;
}
.category-tag {
    display: inline-block;
    padding: 2px 8px;
    background: var(--tag-bg);
    color: var(--tag-color);
    border-radius: 12px;
    font-size: 0.85em;
}

/* Feed subscriptions */
.feeds-dialog {
    max-width: 560px;
    width: 90vw;
}
.feed-list {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin: 8px 0;
    max-height: 320px;
    overflow-y: auto;
}
.feed-item {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 8px;
    padding: 8px;
    border: 1px solid var(--border-light);
    border-radius: 6px;
}
.feed-item-info {
    flex: 1;
    min-width: 0;
}
.feed-item-url {
    font-size: 0.9em;
    font-weight: 500;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.feed-item-meta {
    font-size: 0.8em;
    color: var(--text-subtle);
    display: flex;
    gap: 6px;
    flex-wrap: wrap;
    margin-top: 2px;
}
.feed-calendar {
    background: var(--tag-bg);
    color: var(--tag-color);
    padding: 0 6px;
    border-radius: 8px;
}
.feed-disabled {
    background: var(--feed-disabled-bg);
    color: var(--feed-disabled-color);
    padding: 0 6px;
    border-radius: 8px;
}
.feed-item-error {
    font-size: 0.8em;
    color: var(--error-color);
    margin-top: 2px;
}
.feed-item-actions {
    display: flex;
    gap: 4px;
    flex-shrink: 0;
}
.feed-action-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: none;
    border: 1px solid var(--border);
    border-radius: 6px;
    cursor: pointer;
    padding: 5px 7px;
    color: var(--text-subtle);
}
.feed-action-btn:hover {
    background: var(--hover-bg);
    color: var(--text);
}
.feed-delete-btn:hover {
    background: var(--feed-disabled-bg);
    border-color: var(--error-color);
    color: var(--error-color);
}
.feed-error {
    color: var(--error-color);
    font-size: 0.85em;
    margin: 4px 0;
}
.feed-loading, .feed-empty {
    text-align: center;
    color: var(--text-faint);
    padding: 16px 0;
}
.feed-add-form {
    margin-top: 8px;
    border-top: 1px solid var(--border-light);
    padding-top: 12px;
}
.feed-handler {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    border-top: 1px solid var(--border-light);
    margin-top: 12px;
    padding-top: 12px;
}
.feed-handler span {
    color: var(--text-subtle);
    font-size: 0.8em;
}

/* Confirm dialog */
.confirm-dialog {
    border: none;
    border-radius: 8px;
    padding: 0;
    width: 90%;
    max-width: 360px;
    box-shadow: 0 8px 32px rgba(0,0,0,0.2);
    animation: confirm-fade-in 0.15s ease-out;
    background: var(--surface);
    color: var(--text);
}

@keyframes confirm-fade-in {
    from { opacity: 0; transform: scale(0.95); }
    to { opacity: 1; transform: scale(1); }
}

.confirm-dialog::backdrop {
    background: rgba(0,0,0,0.4);
}

.confirm-dialog-body {
    padding: 24px;
}

.confirm-dialog-title {
    font-size: 1.1rem;
    font-weight: 600;
    margin-bottom: 12px;
    color: var(--text);
}

.confirm-dialog-message {
    font-size: 0.95rem;
    color: var(--text-muted);
    line-height: 1.5;
    margin-bottom: 20px;
    white-space: pre-line;
    overflow-wrap: anywhere;
}

.confirm-dialog-actions {
    display: flex;
    gap: 8px;
    justify-content: flex-end;
}

.confirm-btn {
    padding: 7px 14px;
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text);
    border-radius: 6px;
    cursor: pointer;
    font-size: 0.9rem;
    font-family: inherit;
}

.confirm-btn:hover {
    background: var(--hover-bg);
}

.confirm-btn-primary {
    background: var(--primary);
    color: #fff;
    border-color: var(--primary);
}

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

.confirm-btn-danger {
    background: var(--danger);
    color: #fff;
    border-color: var(--danger);
}

.confirm-btn-danger:hover {
    background: var(--danger);
    opacity: 0.85;
}

/* App layout with sidebar */
.app-layout {
    display: flex;
    gap: 12px;
    min-height: 0;
    flex: 1;
}

.app-main {
    flex: 1;
    min-width: 0;
}

/* Calendar sidebar */
.calendar-sidebar {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 8px;
    /* See .mini-month: the left column scrolls rather than squeezing the two
       panels in it. */
    flex-shrink: 0;
}

.calendar-sidebar-header {
    font-weight: 600;
    font-size: 0.85rem;
    color: var(--text-muted);
    margin-bottom: 8px;
    padding-bottom: 4px;
    border-bottom: 1px solid var(--border-light);
}

.calendar-sidebar-toggle-all {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 4px 0;
    margin-bottom: 4px;
    border-bottom: 1px solid var(--border-light);
    padding-bottom: 8px;
    cursor: pointer;
    font-size: 0.8rem;
    color: var(--text-faint);
    font-style: italic;
}

.calendar-sidebar-toggle-all:hover {
    color: var(--link);
}

.calendar-sidebar-toggle-all input[type="checkbox"] {
    margin: 0;
    cursor: pointer;
}

.calendar-sidebar-item {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 4px 0;
    cursor: pointer;
    font-size: 0.85rem;
    color: var(--text);
}

.calendar-sidebar-item:hover {
    color: var(--link);
}

.calendar-sidebar-item input[type="checkbox"] {
    margin: 0;
    cursor: pointer;
}

.calendar-dot {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    flex-shrink: 0;
}

.calendar-sidebar-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.calendar-edit-trigger {
    display: none;
    margin-left: auto;
    background: none;
    border: none;
    cursor: pointer;
    color: var(--text-disabled);
    padding: 0 2px;
    flex-shrink: 0;
}

.calendar-sidebar-item:hover .calendar-edit-trigger {
    display: inline-flex;
    align-items: center;
}

.calendar-edit-trigger:hover {
    color: var(--link);
}

.calendar-sidebar-edit {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px;
    padding: 4px 0;
}

.calendar-edit-name {
    flex: 1;
    min-width: 0;
    font-size: 0.8rem;
    padding: 2px 4px;
    border: 1px solid var(--border);
    border-radius: 4px;
    background: var(--surface);
    color: var(--text);
}

.calendar-edit-actions {
    display: flex;
    gap: 2px;
    flex-shrink: 0;
}

.calendar-edit-btn {
    display: inline-flex;
    align-items: center;
    background: none;
    border: none;
    cursor: pointer;
    padding: 0 2px;
    color: var(--text-muted);
    flex-shrink: 0;
}

.calendar-edit-btn:hover {
    color: var(--link);
}

.calendar-edit-colors {
    display: flex;
    gap: 4px;
    width: 100%;
    flex-wrap: wrap;
    margin-top: 2px;
}

.calendar-color-swatch {
    width: 18px;
    height: 18px;
    border-radius: 50%;
    cursor: pointer;
    border: 3px solid transparent;
}

.calendar-color-swatch.selected {
    border-color: var(--text);
}

.calendar-color-swatch:hover {
    opacity: 0.8;
}

/* Left sidebar wrapper. Stretched rather than hugging its content so the footer
   below can be pushed to the bottom of the column. */
.left-sidebar {
    /* 200px at the default root font. It sizes the mini month and the calendar
       list, not the footer — .sidebar-footer reaches past it on the left — and
       .brand up in the top bar draws on the same token, so the two cannot drift
       apart. */
    width: var(--sidebar-width);
    flex-shrink: 0;
    display: flex;
    flex-direction: column;
    gap: 8px;
    align-self: stretch;
}

/* Everything above the footer, and the half of the column that scrolls. It is a
   wrapper rather than the footer simply being the last child, because a
   scrollbar in the views where this scrolls would otherwise eat ~15px of the
   footer's width on every platform that draws classic scrollbars — which at the
   time was more than the whole margin the two buttons had. Outside the
   scrollport the footer keeps its full width whatever the panels above do. */
.sidebar-content {
    display: flex;
    flex-direction: column;
    /* Not a duplicate of .left-sidebar's gap: that one now has only two children
       and spaces the content from the footer, this one spaces the mini month
       from the calendar list. Both are load-bearing. */
    gap: 8px;
    /* No `min-height: 0` here on purpose. It belongs only where this scrolls,
       and even there `overflow-y: auto` already zeroes the automatic minimum
       size. In the base rule it would let a content-sized column shrink under
       its own content and spill over the footer. */
}

/* Light/dark toggle and Settings, pinned to the bottom of the left column — the
   same footer MyNotes puts them in (see its .sidebar-footer), and a sibling of
   the scrolling content there as it is here. `margin-top: auto` pushes it down
   in the views where .sidebar-content is content-sized; where that one takes
   `flex: 1` instead there is no free space left and the footer simply follows
   it. `position: sticky` keeps it on screen while the *page* scrolls in the
   month and year views — the three that fill the viewport scroll inside
   .sidebar-content, which the footer is no longer part of.

   It paints --bg — the same colour as everything behind it — and that is a
   deliberate divergence from the MySuite spec, made on the owner's instruction
   after seeing the footer in Firefox: the two controls sit directly on the left
   column's background, and the three apps are accepted to differ in the colour
   immediately behind them. §5.3 used to pin that backdrop to --surface for all
   three; it was withdrawn and rewritten to record a backdrop per app and theme,
   so --bg here is MyCal's *recorded* value rather than an unmet requirement. The
   cross-repo guard is green on it. The obligation did not lapse with the colour:
   the backdrop must still be opaque, still written down before it is changed,
   and everything drawn on it still verified against it.

   THE DECLARATION IS STILL LOAD-BEARING, AND NOW IT LOOKS REDUNDANT. Painting
   --bg is not the same as painting nothing. This footer is sticky, and in the
   month and year views the document scrolls underneath it by several hundred
   pixels; transparent would let the mini month and the calendar list slide
   visibly through the buttons. The colour is redundant, the opacity is not.
   Deleting this line changes nothing on screen until someone scrolls, which is
   exactly why it is spelled out here.

   What removing the fill costs, measured in both themes, is confined to LIGHT.
   Against --bg the resting label falls to 4.393:1 (WCAG 1.4.3 wants 4.5:1) and
   the hover fill lands on 1.000:1, because MyCal's light --bg and --hover-bg are
   both #f3f4f6 — the surface coincidence the shared spec names in §5.4/§10.1,
   biting from the other end. Those two are re-fixed by the --sidebar-footer-*
   aliases in the palette block rather than by putting the panel back; read that
   comment for the numbers. Dark diverges in nothing and improved on every row:
   label 5.782 → 6.987, focus ring 3.991 → 4.823, fill 1.424 → 1.721.

   border-top stays; border-right does not. The two look like a pair and are not.
   The separator is mandated by the shared spec (§8.5) and has never required an
   edge to terminate against — MyMail's and MyNotes' sidebars happen to carry a
   border-right, which is a property of those two apps rather than a requirement
   on this one, and reasoning from the two in view to the third is how a local
   quirk becomes a false rule. The decisive evidence is this repo's own history:
   border-top + --bg + no border-right is what MyCal shipped for the entire life
   of the footer, up to 282f00f, which added the fill and the border-right and
   never touched the separator. Nobody objected to that configuration; the
   objection arrived with the white fill.

   The border-right goes because the condition that earned it has reversed. It
   was added to stop the band terminating against the page with no edge — a seam
   that existed only because the band was a different colour from the page. There
   is no band now, so there is no seam, and the declaration would be carried
   forward on a rationale that has expired.

   The padding is 8px on three sides and zero on the bottom, and it is measured
   against the WINDOW, not this column — see the margin-left comment below and
   the shared spec. (An earlier version of this paragraph described it as an
   inset "from the column's edges on all four sides", which was the
   container-relative rule the spec has since withdrawn; it was already false of
   this code when it was read.) What the padding does still do is stay *inside*
   the sticky box, so the footer's own bottom edge lands on the bottom of the
   view beside it — which is what e2e/tests/calendar-views.spec.ts measures, and
   the reason the bottom inset had to come out of the padding rather than out of
   a margin. */
.sidebar-footer {
    margin-top: auto;
    /* Cancel .app's horizontal padding so the footer reaches the window's left
       edge and its 8px of padding is the *only* thing between the window and
       the buttons. That is the MySuite placement rule: the two controls must
       land at the same window coordinates in all three apps, so that switching
       browser tabs does not make them jump. MyMail and MyNotes get their 8px
       from a sidebar panel already flush at x=0; MyCal's column sits inside
       .app's padding, so the footer alone steps back out of it. The buttons
       keep their 8px inset from the separator's start, exactly as before —
       the separator moves with them rather than staying behind.

       This lands the footer at exactly x=0, and in the three views where .app is
       `overflow: hidden` that is its clipping edge. It clears only because .app
       has no left border; giving it one would shave the footer with nothing to
       catch it, since the tests assert the buttons' own 8px and that would still
       hold. */
    margin-left: calc(-1 * var(--app-padding-x));
    position: sticky;
    bottom: 8px;
    /* No padding on the bottom: .app's own 8px sits below this box, and that is
       what puts the buttons 8px from the window's bottom edge. Padding here as
       well would double it to 16px. The horizontal gap is solved the opposite
       way — margin-left cancels .app's padding and this box supplies the 8px
       itself — because e2e/tests/calendar-views.spec.ts pins the footer box's
       bottom edge to the bottom of the view beside it, so the box may not move
       down even though the buttons must. */
    padding: 8px 8px 0;
    border-top: 1px solid var(--border);
    background: var(--bg);
}

/* nowrap: the two are content-sized and must stay on one row. See the width
   budget noted on .sidebar-footer-btn. */
.sidebar-footer-actions {
    display: flex;
    flex-wrap: nowrap;
    gap: 6px;
}

/* Unlike the icon-only buttons elsewhere in the UI, these carry a label next to
   the glyph — the shape MyNotes uses for the same two actions.

   Geometry is the MySuite canonical spec, shared verbatim with MyMail and
   MyNotes and defined in ../mysuite/spec/sidebar-footer.md — read that before
   changing anything here. The computed outer height must be 29.2px at the
   default 16px root font size (19.2px line box + 8px padding + 2px border).
   The box mixes rem text with px padding, so that number holds at the default
   root size and grows with a larger one — it is the acceptance measurement, not
   a hard contract. Do not change the font-size, padding, line-height or border
   here without changing them in the other two.

   Content-sized rather than stretched: the pair measures 174px inside the
   footer's 200px content box, so 26px spare. (The footer's border box is wider
   than the column — it reclaims .app's horizontal padding — so the buttons get
   the column's width back after their own 8px insets, not minus them.) Treat
   174px as this platform's number, not a constant: it is text measured in
   whatever `system-ui` resolves to, so Segoe UI or SF Pro will not give exactly
   this. With nowrap on both the row and the buttons and no shrink, a font wide
   enough to eat the 26px overflows rather than reflowing. If a label here ever
   changes, re-measure. */
.sidebar-footer-btn {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    background: none;
    border: 1px solid var(--border);
    border-radius: 6px;
    cursor: pointer;
    padding: 4px 8px;
    font-family: inherit;
    /* Two decimals deliberately — do not normalise to 0.8rem. The MySuite spec
       states it this way so one grep finds the rule in all three repos; the
       computed value is identical either way, so nothing would fail. */
    font-size: 0.80rem;
    line-height: 1.5;
    white-space: nowrap;
    /* These four change nothing today — they are what this button already
       computes — and that is the point. Each currently arrives by coincidence
       rather than by contract, from a source that differs between the three
       apps: the weight and style fall out of the UA `button` shorthand here and
       in MyNotes, but MyMail's Settings control is an <a> and inherits them from
       body instead. Same value, three routes, three engines with their own UA
       defaults. An ordinary `button { font-weight: 500 }` added to any one of
       the three would break the match with nothing to catch it.

       flex-shrink is the load-bearing one. Flex items shrink before they
       overflow; these are saved only by `min-width: auto` resolving to the
       nowrap content width, and `min-width: 0` is a common enough flex reset
       that it could reach them. Pinning 0 means the failure mode stays overflow,
       which is loud and visible, rather than a silent squeeze that clips the
       labels with no ellipsis. This column has the least slack of the three. */
    font-weight: 400;
    font-style: normal;
    text-align: center;
    flex-shrink: 0;
    /* --text-subtle in dark, one stop darker in light. The alias, not the token
       the shared spec's role table names, because the light value had to move
       when the footer stopped painting a panel — see the palette block. */
    color: var(--sidebar-footer-text);
    /* The only transition in this stylesheet — every other hover in the app
       snaps. It is mandated by the MySuite spec, so do not delete it for
       consistency with its neighbours. Colour only, no motion, which is why it
       carries no prefers-reduced-motion guard. */
    transition: background 0.12s, color 0.12s, border-color 0.12s;
}

/* The hover border is deliberately not the resting --border: in the dark theme
   --hover-bg and --border are the same #374151, so keeping it would erase the
   button's outline exactly when it lights up.

   The fill resolves through an alias in light. --hover-bg is #f3f4f6 there and
   so is the --bg this footer now paints, so the mandated colour would draw over
   itself at 1.000:1. That is a fill that disappears, not a hover state that
   disappears — this rule moves the label and border colours too, and those
   still read. The alias resolves to --border
   (#e5e7eb) instead: 1.125:1, hover label 11.856:1 on it.

   --surface was the other candidate and was rejected upstream, not here. It is
   the arithmetically better one — #ffffff on #f3f4f6 is the mandated pair with
   the roles swapped, and contrast is symmetric, so it preserves 1.101:1 exactly
   where #e5e7eb makes MyCal a third figure. 1.101 is MyCal's and MyMail's figure,
   not the suite's — MyNotes ships 1.053, so there was never one number here. It lost on two
   non-numeric grounds: a white fill paints a small white box in the footer the
   owner had just asked to stop being a white box, and it would make MyCal's hover
   *lighten* where MyMail's and MyNotes' darken — a difference in how the control
   behaves rather than in what colour sits behind it, which is the kind switching
   tabs actually shows. Recorded because it was a judgement call and the
   arithmetic argued the other way.

   The fill and the resting border are the same #e5e7eb in light, which looks like
   the dark collision the paragraph above warns about. It is not: the hover border
   moves to --text-faint, so the control's boundary survives the fill landing on
   it (2.051:1 against the fill).

   Dark keeps --hover-bg untouched and gained a little: 1.424:1 against the old
   --surface backdrop, 1.721:1 against --bg. Do not strengthen either theme's
   fill to taste — the weakness is mandated and is recorded upstream as a known
   suite-wide limitation, not a MyCal defect. */
.sidebar-footer-btn:hover {
    background: var(--sidebar-footer-hover-bg);
    color: var(--text);
    border-color: var(--text-faint);
}

/* An outline rather than the translucent ring the MySuite spec first called for,
   and the offset is the load-bearing part. (These buttons never shipped with
   that ring — it was rejected before this landed — so there is nothing to find
   in this repo's history.) WCAG 1.4.11 (AA) wants 3:1 for a focus indicator
   against the colours adjacent to it; drawn tight against the button its
   neighbour is --border, and in the dark theme --primary vs --border is
   2.803:1 — a ceiling no amount of opacity can lift. Offsetting by 2px moves
   the indicator clear of the border so both of its neighbours are the footer's
   own background, now --bg: measured 4.696:1 light and 4.823:1 dark.

   Both figures were re-measured when the backdrop moved, rather than assumed to
   still clear. Light fell (5.169 → 4.696) and dark rose (3.991 → 4.823), so the
   binding case is no longer dark. It also means MyCal's dark margin is no longer
   the suite's: MyMail and MyNotes still sit at 3.991 against --surface, roughly
   one point of margin over the 3:1 floor with none to spend, where this repo now
   has 1.823 points of margin. Both of those are margins, not ratios — MyCal's
   ratio is the 4.823:1 above. Do not copy this repo's figure back into the
   shared spec, and do not treat the extra room here as room the other two have.

   It also removes the forced-colors problem rather than patching it: an outline
   is painted under forced colours, a box-shadow is not. Do not add `outline:
   none` back, and do not shrink the 2px — that is the minimum perimeter WCAG
   2.4.13 accepts. */
.sidebar-footer-btn:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}

/* Mini month calendar */
.mini-month {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 6px;
    overflow: hidden;
    /* `overflow: hidden` zeroes the automatic minimum size a flex item would
       otherwise get, so in the views where the left column is height-bound this
       would shrink away to its two borders rather than push the column into its
       own scrollbar. .calendar-sidebar is held to its content for the same
       reason — the column scrolls, its contents do not compress. */
    flex-shrink: 0;
}

.mini-month-header {
    padding: 4px 8px;
    font-weight: 600;
    font-size: 0.8rem;
    background: var(--surface-alt);
    border-bottom: 1px solid var(--border-light);
    display: flex;
    align-items: center;
    justify-content: space-between;
}

.mini-month-title {
    cursor: pointer;
}

.mini-month-title:hover {
    color: var(--link);
}

.mini-month-nav {
    display: inline-flex;
    align-items: center;
    background: none;
    border: none;
    cursor: pointer;
    padding: 2px;
    color: var(--text-muted);
    border-radius: 4px;
}

.mini-month-nav:hover {
    background: var(--accent-hover-bg);
    color: var(--link);
}

.mini-month-grid {
    padding: 2px 4px 3px;
}

.mini-month-weekday-row {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    text-align: center;
    font-size: 0.55rem;
    color: var(--text-disabled);
    font-weight: 600;
}

.mini-month-weekday {
    padding: 1px 0;
    line-height: 1.8;
}

.mini-month-week-row {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    text-align: center;
    align-items: center;
}

.mini-month-day {
    font-size: 0.65rem;
    line-height: 2.2;
    border-radius: 50%;
    text-align: center;
}

.mini-month-day:hover {
    background: var(--hover-bg);
}

.mini-month-day-other {
    color: var(--text-placeholder);
}

.mini-month-day-today {
    background: var(--primary);
    color: #fff;
    font-weight: 600;
    width: 20px;
    height: 20px;
    line-height: 20px;
    border-radius: 50%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin: auto;
}

.api-key-error {
    color: var(--error-color);
    font-size: 0.8rem;
    margin: -8px 0 8px;
}

.settings-hint {
    display: block;
    font-size: 0.8rem;
    color: var(--text-faint);
    margin-top: 4px;
}

.input-narrow {
    width: 60px;
}

.map-link {
    display: inline-block;
    margin: 4px 0 8px;
    color: var(--primary);
}

.url-link {
    color: var(--primary);
    word-break: break-all;
    margin-left: 4px;
}

.rich-editor-loading {
    padding: 8px;
    color: var(--text-subtle);
    font-size: 0.85rem;
}

.is-hidden {
    display: none;
}

.map-picker {
    margin: 8px 0;
    position: relative;
    overflow: hidden;
    border-radius: 6px;
}

.map-loading {
    padding: 12px;
    color: var(--text-subtle);
}

.map-canvas {
    width: 100%;
    height: 200px;
}

.map-canvas--editing {
    height: 250px;
}

.is-clickable {
    cursor: pointer;
}

.share-form {
    padding: 8px 0 4px;
    border-top: 1px solid var(--border);
    margin-top: 4px;
}

.share-input-row {
    display: flex;
    gap: 8px;
    margin-top: 4px;
    align-items: center;
}

.share-input-row input[type="email"] {
    flex: 1;
    min-width: 0;
    /* The row already carries the gap below the label. */
    margin-top: 0;
}

/* The input takes the slack; the buttons keep their natural width rather than
   being squeezed to the label ("Send" is short, and swaps to "Sending…"). */
.share-input-row button {
    flex: 0 0 auto;
    white-space: nowrap;
}

.share-success {
    color: var(--success-color);
    font-size: 0.85em;
    margin-top: 4px;
}

.share-error {
    color: var(--danger);
    font-size: 0.85em;
    margin-top: 4px;
}

/* MyNotes integration: note picker (edit mode) and the render-kit frame that
   shows the linked note's content (view mode). */
.note-picker {
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.note-picker > span {
    font-size: 0.85em;
    color: var(--text-muted);
}

.note-picker-selected {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
}

.note-picker-results {
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: 4px;
    max-height: 180px;
    overflow-y: auto;
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 4px;
}

.note-picker-results button {
    width: 100%;
    text-align: left;
}

.note-picker-empty {
    font-size: 0.85em;
    color: var(--text-faint);
}

.note-picker-error,
.note-panel-error {
    font-size: 0.85em;
    color: var(--error-color);
}

/* A note is worth reading at full size, so the dialog runs the whole viewport
   height whenever one is on screen and the frame claims every pixel the rest
   of the form leaves over. `min-height` rather than `height` on the form so a
   form too tall for the viewport still overflows into the dialog's scroll. */
.event-dialog:has(.note-panel) {
    height: 100dvh;
}

.event-dialog:has(.note-panel) > form {
    display: flex;
    flex-direction: column;
    min-height: 100%;
}

.note-panel {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-height: 0;
    padding: 8px 0 4px;
    border-top: 1px solid var(--border);
    margin-top: 4px;
}

.note-panel-header {
    display: flex;
    align-items: baseline;
    gap: 6px;
    margin-bottom: 6px;
}

/* Sized by the flex column above, not by the framed document's content. */
.note-panel-frame {
    width: 100%;
    flex: 1;
    min-height: 120px;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--surface);
}

/* Demo mode (see web/ts/components/DemoDialog.tsx). Only ever rendered by a
   build that has no server behind it.

   It carries the same sentence MyMail and MyNotes carry, at their 0.75rem, but
   it is a button rather than their inert span/paragraph — clicking it brings
   the demo notice back. Hence `font-family: inherit`: a <button> otherwise
   renders in the UA's own font, which the uppercase chip this used to be got
   away with and a sentence of body copy does not. The uppercase and the
   letter-spacing went with the wording, for the same reason.

   Text is --tag-color on --tag-bg: 5.493:1 light (#1d4ed8 on #dbeafe), 6.379:1
   dark (#93c5fd on #1e3a5f). The border is the same colour as the text against
   the top bar's --bg, 6.090:1 light and 9.838:1 dark. Those four are computed
   from the tokens, not read off a screen.

   Why the sentence is not simply always shown, unlike MyMail's and MyNotes'.
   Both of those sit in containers with room to spare — MyMail's top bar holds a
   title and nothing else, MyNotes' badge has the sidebar's full width. MyCal's
   top bar holds the brand block, the whole navigation and the search field, and
   below a certain window width there is no room. Measured on Chromium/Linux in
   Liberation Sans, so readings and not constants, and quoted for the Schedule
   view because its nav is the widest of the five: the sentence wants 271px at a
   16px root, and the bar's spare width with the badge removed entirely is 295px
   at a 1440px window, 135px at 1280px, 7px at 1152px, and negative at 1024px
   and below, where the search field is already at its 110px floor.

   A shorter wording would buy a little and not enough — the shortest tried,
   "Demo — this browser only", wants 161px, which clears 1440px and 1366px but
   not the 135px going at 1280px, so it would need a breakpoint of its own and
   would still say nothing below one. Deliberately not done; one string.

   Letting the badge absorb the squeeze instead was tried and is far worse than
   an overflow: it collapsed to 18px wide and 221px tall, nine lines of one
   character, and took the top bar from 40px to 221px with it. */
.demo-badge {
    background: var(--tag-bg);
    color: var(--tag-color);
    border: 1px solid var(--tag-color);
    border-radius: 6px;
    font-family: inherit;
    font-size: 0.75rem;
    font-weight: 500;
    line-height: 1.3;
    /* A button centres its text by default, which reads oddly the moment the
       sentence takes a second line. Logical, unlike .sidebar-footer-btn's
       `center` — that one is pinned by the shared spec, this one is free. */
    text-align: start;
    cursor: pointer;
    padding: 4px 8px;
}

/* 89.9375rem = 1439px at the browser's default 16px: below the narrowest window
   measured above with room for the sentence, the clause is hidden from the eye
   but not from the accessibility tree. `display: none` would take it out of
   both, and the sentence is the part worth keeping for a screen reader at every
   width. It is the button's accessible name throughout; the visible "Demo" is a
   substring of it, which is what WCAG 2.5.3 asks for.

   The query is this way round on purpose. Written as `min-width` it would have
   to *un-set* all five properties at wide widths, and a sixth added here later
   would silently fail to be undone. This way the clipping lives in one place and
   above the breakpoint the span is simply inline text.

   Deliberately a `rem` query: relative units in a media query resolve against
   the browser's *initial* font size rather than the page's, so a reader whose
   default is 20px gets the breakpoint at 1799px and one at 24px gets it at
   2159px — pushed out by the same factor the sentence grows by.

   There is deliberately no `min-width` floor on the badge. A floor makes the
   wrapped shape tidier and is the obvious thing to add, but it reintroduces a
   horizontal scroll whenever the page's root font has been raised without the
   browser default moving with it — the two are independent and the query can
   only see the second. Without one, `min-width: auto` on a flex item stops the
   shrinking at the longest word, so the worst case is a tall narrow badge
   rather than an overflow. Do not add `min-width: 0` here: that is what removes
   the stop, and it is what produced the 18px × 221px collapse above. */
@media (max-width: 89.9375rem) {
    .demo-badge-detail {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
    }
}

/* --primary-soft laid *over* the resting fill rather than replacing it, which
   is what makes this move the right way in both themes: the tint darkens
   #dbeafe to #c5d9fb in light and lightens #1e3a5f to #23477a in dark. Naming a
   fill directly cannot do that — the rule used to say `--accent-hover-bg`, which
   in dark is the same #1e3a5f as --tag-bg, so hovering did nothing at all
   (1.000:1), and in light it went *lighter*, reading as de-emphasis.

   The two hovered hexes are read off rendered pixels, not computed from the
   alpha: light lands a shade off the arithmetic. Against the resting fill they
   measure 1.172:1 light and 1.234:1 dark — small, as a hover tint should be,
   but they are the numbers, so re-measure rather than assume the tint still
   shows if --primary-soft or --tag-bg moves. The label keeps its contrast on
   top of the hover fill: 4.689:1 light and 5.167:1 dark, both over 4.5:1. */
.demo-badge:hover {
    background: linear-gradient(var(--primary-soft), var(--primary-soft)) var(--tag-bg);
}

/* The pattern .sidebar-footer-btn uses, and for the same reason: offset clear of
   the badge's own border so both of the indicator's neighbours are the top bar's
   --bg, which --primary meets at 4.696:1 light and 4.823:1 dark. An outline
   rather than a box-shadow so it survives forced colours. */
.demo-badge:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}

.demo-dialog {
    border: none;
    border-radius: 8px;
    padding: 24px;
    width: 90%;
    max-width: 480px;
    box-shadow: 0 8px 32px rgba(0,0,0,0.2);
    background: var(--surface);
    color: var(--text);
}

.demo-dialog::backdrop {
    background: rgba(0,0,0,0.4);
}

.demo-dialog p {
    margin-bottom: 12px;
    font-size: 0.9rem;
    line-height: 1.5;
    color: var(--text-muted);
}

/* Device consent must keep requester details readable on narrow screens. */
.device-approval { margin-top: 1.5rem; }
.device-approval label { display: block; margin: 0.5rem 0; }
.device-approval input[type="checkbox"] { width: auto; margin-right: 0.5rem; }
.device-requester { overflow-wrap: anywhere; }
.device-actions { display: flex; gap: 0.5rem; margin-top: 0.75rem; }

.device-approval input[type="text"] { box-sizing: border-box; width: 100%; max-width: 100%; }
.device-approval fieldset { min-width: 0; }
.device-approval dd { margin-left: 0; margin-bottom: 0.5rem; overflow-wrap: anywhere; }
.device-approval dt { font-weight: 600; }

/* The dedicated device page shares the palette, without the app layout. */
.device-page { min-height: 100vh; height: auto; overflow: auto; }
.device-page-content { max-width: 42rem; margin: 2rem auto; padding: 1.5rem; background: var(--surface); border: 1px solid var(--border); border-radius: 0.5rem; }
.device-page-content h1 { font-size: 1.4rem; }
.device-page-content .device-approval > p { margin: 0.75rem 0; }
.device-page-content input[type="text"] { padding: 0.5rem; border: 1px solid var(--border); border-radius: 0.375rem; background: var(--surface); color: var(--fg); }
@media (max-width: 600px) { .device-page-content { margin: 0.75rem; padding: 1rem; } }
.device-page-content .device-approval h2 { font-size: 1.15rem; }

/* Settings sections and scoped token management. */
.settings-tabs {
    display: flex;
    gap: 8px;
    margin-bottom: 16px;
    border-bottom: 1px solid var(--border);
    padding-bottom: 8px;
}
.settings-tabs [aria-selected="true"] {
    background: var(--primary-soft);
    border-color: var(--primary);
    color: var(--text);
}
.api-token-list {
    padding-left: 20px;
}
.api-token-list li {
    margin-bottom: 16px;
    overflow-wrap: anywhere;
}
.api-token-list p {
    margin: 4px 0 8px;
}

/* Keep the source as a muted marker until the saved position is rendered. */
.week-event.drag-source,
.allday-event.drag-source {
    opacity: 0.3;
    pointer-events: none;
}

.week-event.drag-preview,
.allday-event.drag-preview {
    opacity: 1;
    position: absolute;
    pointer-events: none;
    z-index: 10;
}

.drag-pending {
    cursor: progress;
}
