/*
 * The phase-3 stylesheet. One file, hand-written, no framework and no CDN.
 *
 * Three reasons, in order. The VPS is standalone (decision #19), so a CDN is an
 * outside dependency this deployment does not otherwise have. The whole UI is
 * about eight screens, which is less CSS than a framework's own configuration.
 * And Caddy serves this file straight from a volume — one request, cached for an
 * hour — where a framework would be the largest thing the site sends.
 */

/* --------------------------------------------------------------------------
 * Tokens
 *
 * Three ways in, and the order they are declared in is the order they win:
 *
 *   1. `:root` — the light theme, and the default for a device that has not
 *      asked for anything.
 *   2. `@media (prefers-color-scheme: dark)` — the device's answer.
 *   3. `:root[data-theme="…"]` — the reader's answer, stamped on <html> by
 *      `theme.js` before the first paint. Declared last so it beats the media
 *      query in both directions: somebody on a dark phone who chose light gets
 *      light, which a media query alone cannot express.
 *
 * The light theme is built on #fff4fb, a warm pink-white, and the accent is a
 * plum that sits on it at better than 4.5:1. The dark theme takes the same
 * accent family rather than the blue it used to have — two themes are one
 * product, and an application that changes its accent hue when the sun goes
 * down reads as two applications.
 * ------------------------------------------------------------------------ */

:root {
    --bg: #fff4fb;
    --surface: #fffafd;
    --border: #ecd6e4;
    --text: #2a1f27;
    --muted: #6c5a66;

    --accent: #a5246d;
    --accent-text: #ffffff;
    /* A shade darker than the accent, for text on the page background. The
     * accent itself is tuned for a white button; a link is small type on pink,
     * which needs the extra contrast. */
    --link: #8e1a5c;
    --link-visited: #6f3b8e;

    /* The status palette. Chosen so present/absent/leave stay distinguishable
     * without colour alone — every one of them is also labelled in text,
     * because a percentage somebody disputes must be readable by a person who
     * cannot tell the two greens apart. */
    --present: #1a7f4b;
    --present-bg: #e4f5ec;
    --half: #8a5a00;
    --half-bg: #fbf0d8;
    --leave: #6639ba;
    --leave-bg: #efe7fb;
    --absent: #c0392b;
    --absent-bg: #fceae9;
    --closed-bg: #f6e6ef;

    /* The dragged selection. Deliberately not one of the status colours: a lit
     * square means "you picked this", which is a different kind of fact from
     * "you were present", and the two appear on the same square at once. */
    --picked: #d4368f;
    --picked-bg: #fbdcee;

    --radius: 6px;
    --gap: 1rem;

    /* Poppins, with the old system stack behind it. The fallback is not
     * decoration: the font comes from fonts.googleapis.com, so a deployment that
     * cannot reach it — or a browser that blocks third-party fonts — renders in
     * the stack this project used before, rather than in the browser's serif
     * default. */
    --font-sans: "Poppins", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}

@media (prefers-color-scheme: dark) {
    :root {
        --bg: #16121a;
        --surface: #1f1a24;
        --border: #372c3d;
        --text: #f0e6ee;
        --muted: #ac9dab;

        --accent: #e879b8;
        --accent-text: #2a1020;
        --link: #f2a2cd;
        --link-visited: #c9a6ea;

        --present: #57d18d;
        --present-bg: #14301f;
        --half: #e0b341;
        --half-bg: #322709;
        --leave: #b799f5;
        --leave-bg: #241a3a;
        --absent: #ff8071;
        --absent-bg: #351613;
        --closed-bg: #241d29;

        --picked: #f58ec7;
        --picked-bg: #3d1c30;
    }
}

/* The reader's explicit choice. Repeated in full rather than layered on the
 * blocks above, because a selector that only fills in the differences would
 * depend on which of the two ran first — and `data-theme="light"` on a dark
 * device has to undo every value the media query set, not some of them. */
:root[data-theme="light"] {
    --bg: #fff4fb;
    --surface: #fffafd;
    --border: #ecd6e4;
    --text: #2a1f27;
    --muted: #6c5a66;

    --accent: #a5246d;
    --accent-text: #ffffff;
    --link: #8e1a5c;
    --link-visited: #6f3b8e;

    --present: #1a7f4b;
    --present-bg: #e4f5ec;
    --half: #8a5a00;
    --half-bg: #fbf0d8;
    --leave: #6639ba;
    --leave-bg: #efe7fb;
    --absent: #c0392b;
    --absent-bg: #fceae9;
    --closed-bg: #f6e6ef;

    --picked: #d4368f;
    --picked-bg: #fbdcee;
}

:root[data-theme="dark"] {
    --bg: #16121a;
    --surface: #1f1a24;
    --border: #372c3d;
    --text: #f0e6ee;
    --muted: #ac9dab;

    --accent: #e879b8;
    --accent-text: #2a1020;
    --link: #f2a2cd;
    --link-visited: #c9a6ea;

    --present: #57d18d;
    --present-bg: #14301f;
    --half: #e0b341;
    --half-bg: #322709;
    --leave: #b799f5;
    --leave-bg: #241a3a;
    --absent: #ff8071;
    --absent-bg: #351613;
    --closed-bg: #241d29;

    --picked: #f58ec7;
    --picked-bg: #3d1c30;
}

/* --------------------------------------------------------------------------
 * Frame
 * ------------------------------------------------------------------------ */

* { box-sizing: border-box; }

/* `hidden` means hidden.
 *
 * The browser's own rule is `[hidden] { display: none }` — a single attribute
 * selector, which any class rule setting `display` silently beats. That has bitten
 * this stylesheet twice: a `display: flex` on the marking buttons left them
 * showing inside every square, and the same on the sheet left it laid out and
 * hittable while it looked closed. Both were invisible in the markup, which said
 * `hidden` and meant it.
 *
 * `!important` is deliberate and is the one place in this file that uses it. The
 * attribute is a statement that an element is not currently part of the page, and
 * nothing about how a component looks when it *is* showing should be able to
 * overrule that. */
[hidden] { display: none !important; }

body {
    margin: 0;
    background: var(--bg);
    color: var(--text);
    /* Poppins runs a little small and a little tight at the sizes this
     * interface uses, so the line height goes up with it. */
    font: 400 16px/1.55 var(--font-sans);
}

/* Every link in the page body. Its absence was the bug: only the header's own
 * links were ever coloured, so an ordinary `<a>` fell back to the browser's
 * default navy — which on a dark surface is very nearly the surface. Declared
 * once, here, rather than per screen, so a link added to any template is
 * legible in both themes without anybody remembering to style it. */
a {
    color: var(--link);
    text-decoration-thickness: 1px;
    text-underline-offset: 0.15em;
}

a:visited { color: var(--link-visited); }
a:hover { color: var(--accent); }

/* The exceptions, and they are all elements that are links only structurally.
 * Left unset above rather than given `!important` here, because a component
 * that wants its own colour should say so in its own rule. */
.brand,
.site-nav a,
.notif-link,
.button,
.calendar .day a { color: inherit; }

.brand:visited,
.site-nav a:visited,
.notif-link:visited,
.button:visited { color: inherit; }

/* Screen-reader-only text. Used by the theme toggle, whose buttons are glyphs. */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

.site-header {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--gap);
    padding: 0.75rem 1.25rem;
    background: var(--surface);
    border-bottom: 1px solid var(--border);
}

.brand {
    font-weight: 600;
    text-decoration: none;
    color: var(--text);
}

.site-nav {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem 1rem;
    flex: 1;
}

/* Buttons rather than words. Each link is a pill with its own edge, which makes
 * it a target with a findable boundary instead of a run of text a thumb has to
 * land inside. */
.site-nav a {
    color: var(--muted);
    text-decoration: none;
    padding: 0.35rem 0.85rem;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--bg);
    font-size: 0.9375rem;
    line-height: 1.35;
    transition: background 120ms ease-out, color 120ms ease-out,
                border-color 120ms ease-out;
}

.site-nav a:hover {
    color: var(--accent);
    border-color: var(--accent);
}

/* Where you are. The only thing on any page that says so, which is why it is a
 * filled pill rather than a shade of grey — and why `aria-current="page"` goes
 * on the same element, since colour alone tells a screen reader nothing. */
.site-nav a.is-here {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--accent-text);
    font-weight: 600;
}

.site-nav a.is-here:hover { color: var(--accent-text); }

/* --------------------------------------------------------------------------
 * The account chip, and the menu under it
 *
 * These two used to be the only unstyled things in the header: the username as
 * bare muted text, and sign-out as an underlined link — beside a row of pills,
 * a theme toggle and an alerts button. Worse, the name was `display: none`
 * below the phone breakpoint, so on the device most of the office actually uses
 * there was nothing at all saying which account was signed in.
 *
 * Now it is one control: an avatar, the name, and a caret, opening a small menu.
 * On a phone the name and caret drop away and the avatar alone remains — which
 * is how the identity survives a 390px header instead of being hidden by it.
 * ------------------------------------------------------------------------ */

.user-menu { position: relative; }

.user-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.25rem 0.6rem 0.25rem 0.3rem;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--surface);
    color: inherit;
    font-size: 0.875rem;
    font-family: inherit;
    cursor: pointer;
}

.user-chip:hover { border-color: var(--accent); }

/* Open is a state worth seeing: without it the panel appears to belong to
 * nothing in particular. */
.user-chip[aria-expanded="true"] {
    border-color: var(--accent);
    background: var(--bg);
}

.user-chip-name {
    font-weight: 600;
    /* A long username must not push the theme toggle off the bar. */
    max-width: 10rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.user-chip-caret { color: var(--muted); font-size: 0.7rem; }

/* The avatar itself — a photo when there is one, initials when there is not.
 * Both are the same size and shape so the header does not move when somebody
 * uploads a picture for the first time. */
.avatar {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.75rem;
    height: 1.75rem;
    border-radius: 50%;
    /* `cover`, so a photo that is not square is cropped rather than squashed —
     * a stretched face is worse than a cropped one. */
    object-fit: cover;
    flex: 0 0 auto;
}

.avatar-initials {
    background: var(--accent);
    color: var(--accent-text);
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.01em;
    /* The letters are decorative — the name is beside them in the chip, and on
     * the profile screen the heading says it. `aria-hidden` in the markup keeps
     * a screen reader from spelling them out. */
    text-transform: uppercase;
}

/* The bigger one on the profile screen. */
.avatar-lg {
    width: 6rem;
    height: 6rem;
    font-size: 1.75rem;
}

.user-menu-panel {
    position: absolute;
    top: calc(100% + 0.4rem);
    /* Anchored to the right edge: the chip is the last thing in the bar, so a
     * left-anchored panel would hang off the window on a narrow screen. */
    right: 0;
    min-width: 11rem;
    z-index: 50;

    display: flex;
    flex-direction: column;
    padding: 0.3rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: 0 12px 32px rgba(0, 0, 0, 0.22);
}

/* One rule for the link and the button, because they must be
 * indistinguishable: a menu where one item is a link and the other is a form
 * submit should not look like two different kinds of thing. */
.user-menu-item {
    display: block;
    width: 100%;
    padding: 0.5rem 0.65rem;
    border: none;
    border-radius: calc(var(--radius) - 2px);
    background: none;
    color: inherit;
    font: inherit;
    font-size: 0.875rem;
    text-align: left;
    text-decoration: none;
    cursor: pointer;
}

.user-menu-item:hover { background: var(--bg); }

/* Signing out is the one destructive thing in the menu, so it is the one thing
 * tinted — and it is separated from the rest by a rule, so it cannot be hit by
 * somebody aiming at the item above it. */
.user-menu-item.is-signout {
    color: var(--absent);
    border-top: 1px solid var(--border);
    border-radius: 0 0 calc(var(--radius) - 2px) calc(var(--radius) - 2px);
    margin-top: 0.3rem;
}

/* --------------------------------------------------------------------------
 * Theme toggle
 *
 * Two buttons rather than a select: it is two states, and a select on a phone
 * opens a full-screen picker to choose between two glyphs. There used to be a
 * third for "follow the device", which is still what happens until one of these
 * is pressed — it was removed because on a device already set to light it did
 * nothing visible, and a control that appears inert is worse than no control.
 * ------------------------------------------------------------------------ */

.theme-toggle {
    display: inline-flex;
    border: 1px solid var(--border);
    border-radius: 999px;
    overflow: hidden;
}

.theme-toggle button {
    border: none;
    border-radius: 0;
    background: transparent;
    color: var(--muted);
    padding: 0.25rem 0.6rem;
    font-size: 0.9rem;
    line-height: 1.4;
}

.theme-toggle button + button { border-left: 1px solid var(--border); }
.theme-toggle button:hover { color: var(--text); }

/* Marked by `theme.js`, which also sets `aria-pressed` — colour alone would not
 * tell somebody using a screen reader which of the three is in force. */
.push-toggle {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    border-radius: 999px;
    padding: 0.3rem 0.75rem;
    font-size: 0.8125rem;
    background: var(--bg);
    color: var(--muted);
    white-space: nowrap;
}

.push-toggle:hover { color: var(--text); }

/* Subscribed. Filled like the current nav pill, because it is the same kind of
 * statement: this is on. */
.push-toggle[data-push-state="on"] {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--accent-text);
}

/* Blocked, unsupported, or no keys on the server. Nothing pressing it can do,
 * so it stops looking like something to press. */
.push-toggle:disabled {
    opacity: 0.55;
    cursor: default;
}

.theme-toggle button.is-active {
    background: var(--accent);
    color: var(--accent-text);
}

.page {
    max-width: 62rem;
    margin: 0 auto;
    padding: 1.5rem 1.25rem 4rem;
}

/* The calendar, and only the calendar. A grid of seven columns gets better the
 * wider it is; a form does not, and neither does a paragraph — which is why this
 * is a class one template opts into rather than a change to `.page`. */
.page-wide {
    max-width: none;
    padding-left: 1rem;
    padding-right: 1rem;
}

h1 { font-size: 1.5rem; margin: 0 0 0.25rem; }
h2 { font-size: 1.125rem; margin: 2rem 0 0.75rem; }

.lede { color: var(--muted); margin: 0 0 1.5rem; }

/* --------------------------------------------------------------------------
 * Surfaces
 * ------------------------------------------------------------------------ */

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

/* Arrived here from a notification, which linked to one particular card by its
 * fragment. The browser has already scrolled to it; this is what says "this one"
 * once it has, because a list of identical cards gives no other clue which of
 * them the message was about.
 *
 * `:target` rather than a class, so nothing has to render differently and no
 * script is involved — and it clears itself the moment the reader navigates
 * anywhere else, which is exactly how long the highlight should last. */
.card:target {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--picked-bg);
}

.messages { list-style: none; margin: 0 0 1.25rem; padding: 0; }

.message {
    padding: 0.75rem 1rem;
    border-radius: var(--radius);
    border: 1px solid var(--border);
    background: var(--surface);
    margin-bottom: 0.5rem;
}

.message-success { border-color: var(--present); background: var(--present-bg); }
.message-error { border-color: var(--absent); background: var(--absent-bg); }
.message-warning { border-color: var(--half); background: var(--half-bg); }

.empty {
    color: var(--muted);
    padding: 1.5rem;
    text-align: center;
}

/* --------------------------------------------------------------------------
 * Controls
 * ------------------------------------------------------------------------ */

button, .button {
    font: inherit;
    padding: 0.5rem 1rem;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--surface);
    color: var(--text);
    cursor: pointer;
    text-decoration: none;
    display: inline-block;
}

button:hover, .button:hover { border-color: var(--accent); }

.button-primary, button.primary {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--accent-text);
}

/* --------------------------------------------------------------------------
 * The profile screen
 * ------------------------------------------------------------------------ */

.profile-photo-card {
    display: flex;
    align-items: flex-start;
    gap: 1.5rem;
    flex-wrap: wrap;
}

.profile-photo-controls { flex: 1 1 16rem; }
.profile-photo-controls h2 { margin-top: 0; }

/* Its own form, so it needs its own separation from the upload button above. */
.profile-photo-remove { margin-top: 0.75rem; }

/* Name/value pairs, as a grid rather than a table: this is a definition list
 * and it should stay one for a screen reader, but the default `dl` stacks
 * every term above its value and wastes the width. */
.detail-list {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 0.4rem 1.25rem;
    margin: 0 0 1rem;
}

.detail-list dt { color: var(--muted); font-size: 0.875rem; }
.detail-list dd { margin: 0; }

.link-button {
    border: none;
    background: none;
    color: var(--muted);
    padding: 0;
    text-decoration: underline;
}

form .field { margin-bottom: 1rem; }
form label { display: block; font-weight: 600; margin-bottom: 0.25rem; }

/* Every text-like input, listed by type because `input` on its own would also
 * catch checkboxes, radios and submit buttons.
 *
 * `email` was missing, which is the whole of bug 1: the one email field in the
 * application — the employee creation form — fell through to the browser's
 * default box, so it sat next to four styled inputs looking like a different
 * form. `url` and `tel` are here for the same reason before anybody adds one:
 * the failure is silent, and it looks like a mistake in the template rather
 * than a gap in a selector list. */
input[type="text"], input[type="date"], input[type="number"],
input[type="password"], input[type="email"], input[type="url"],
input[type="tel"], input[type="search"], select, textarea {
    font: inherit;
    width: 100%;
    max-width: 24rem;
    padding: 0.5rem;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--surface);
    color: var(--text);
}

/* One visible focus ring for everything focusable, in the accent rather than
 * the browser's default blue — which is invisible against the dark surface for
 * the same reason the unstyled links were. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
.day:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
}

.errorlist {
    list-style: none;
    margin: 0.25rem 0 0;
    padding: 0;
    color: var(--absent);
    font-size: 0.875rem;
}

.helptext { color: var(--muted); font-size: 0.875rem; }

.inline-form {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: 0.75rem;
}

.inline-form .field { margin-bottom: 0; }
.inline-form input, .inline-form select { width: auto; }

/* --------------------------------------------------------------------------
 * Tables
 * ------------------------------------------------------------------------ */

.table-scroll { overflow-x: auto; }

table {
    width: 100%;
    border-collapse: collapse;
    background: var(--surface);
}

th, td {
    text-align: left;
    padding: 0.5rem 0.75rem;
    border-bottom: 1px solid var(--border);
    white-space: nowrap;
}

th { font-size: 0.8125rem; text-transform: uppercase; color: var(--muted); }

td.numeric, th.numeric { text-align: right; font-variant-numeric: tabular-nums; }

/* --------------------------------------------------------------------------
 * Month grid
 *
 * The screen a plain admin changelist cannot produce (plan §10): a calendar
 * whose ordinary days have no rows, because rules are computed and only
 * decisions are stored. It is now also the marking screen and the leave
 * picker, so a square is a target as well as a statement.
 * ------------------------------------------------------------------------ */

.month-nav {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--gap);
    margin-bottom: 1rem;
}

/* The month is the heading of this screen and used to be set in the body size,
 * which left the largest thing on the page as the word "Calendar" above it —
 * a title that says the same as the tab it is in. */
.month-nav strong {
    font-size: clamp(1.5rem, 3.2vw, 2.25rem);
    font-weight: 600;
    letter-spacing: -0.01em;
    line-height: 1.15;
}

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

    /* Not `overflow: hidden`, and that is a deliberate trade. A pressed square
     * scales up and casts a shadow, and clipping the grid would shave that flat
     * for every cell on an edge — which is most of them. The cost is that the
     * four corner cells square off the container's rounding by a pixel or two;
     * the alternative is a lift that works everywhere except the outside row. */
    overflow: visible;

    /* Dragging across the grid must not scroll the page or start a text
     * selection — which is what a finger dragged across a page normally does,
     * and is the single thing that makes every gesture here work on a phone. */
    touch-action: none;
    user-select: none;
    -webkit-user-select: none;
}

.calendar .weekday {
    background: var(--surface);
    color: var(--muted);
    font-size: 0.75rem;
    text-transform: uppercase;
    padding: 0.4rem;
    text-align: center;
}

.calendar .day {
    background: var(--surface);
    /* Tied to the width rather than fixed, which is what stops the grid looking
     * squeezed now that it runs the full width of the window: a row that stays
     * 5.5rem tall while its columns grow to 200px wide reads as a spreadsheet,
     * not a calendar. The floor keeps a narrow window usable and the ceiling
     * stops a very wide monitor producing squares the size of postcards. */
    min-height: clamp(6rem, 9.5vw, 10rem);
    padding: 0.5rem 0.55rem;
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
    font-size: 0.8125rem;
    position: relative;
}

/* The date is the thing being scanned for, so it is set like one. */
.calendar .day .num {
    font-size: 1.0625rem;
    font-weight: 600;
    line-height: 1.2;
}

/* Leading and trailing blanks, so the 1st lands under the right weekday. */
.calendar .day.blank { background: var(--bg); min-height: 3rem; }

.calendar .day .num { font-weight: 600; }
.calendar .day .note { color: var(--muted); font-size: 0.75rem; }
.calendar .day.today { outline: 2px solid var(--accent); outline-offset: -2px; }

/* Status colouring, always paired with a text label in the cell itself. */
.day.is-present { background: var(--present-bg); }
.day.is-half_day { background: var(--half-bg); }
.day.is-leave { background: var(--leave-bg); }
.day.is-absent { background: var(--absent-bg); }
.day.is-not_owed { background: var(--closed-bg); }

.badge {
    display: inline-block;
    font-size: 0.6875rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.02em;
    padding: 0.1rem 0.35rem;
    border-radius: 999px;
    border: 1px solid currentColor;
}

.badge-present { color: var(--present); }
.badge-half_day { color: var(--half); }
.badge-leave { color: var(--leave); }
.badge-absent { color: var(--absent); }
.badge-not_owed { color: var(--muted); }
.badge-pending { color: var(--accent); }

/* --------------------------------------------------------------------------
 * Picking days, and pressing them
 *
 * The whole square is the target. It used to be a checkbox in the corner of the
 * cell with a "Pick" label beside it — a 13-pixel box on a phone, and two
 * separate things to look at on every one of thirty-one squares.
 * ------------------------------------------------------------------------ */

.calendar .day[data-date] { cursor: pointer; }

.calendar .day.unselectable:not([data-markable]) { opacity: 0.62; }

/* The lift. A square grows and casts a shadow while it is held, so a tap feels
 * like pressing something rather than like waiting to see whether it worked.
 *
 * This is why the grid above is `overflow: visible`: a raised square has to be
 * allowed outside its own cell, and `overflow: hidden` would shave the top row
 * flat while lifting the others. */
.calendar .day.is-pressed {
    transform: scale(1.06);
    z-index: 3;
    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18);
}

.calendar .day {
    transition: transform 120ms ease-out, box-shadow 120ms ease-out;
}

/* A picked square. The whole cell lights, which is the point of the gesture: a
 * dragged run reads as one block rather than as five ticked boxes. Marked by an
 * inset ring as well as by fill, so a run stays legible where the fill competes
 * with a status colour underneath it. */
.calendar .day.is-picked {
    background: var(--picked-bg);
    box-shadow: inset 0 0 0 2px var(--picked);
    /* The trail. Only squares that were not already lit run this, because the
     * class is toggled rather than rewritten — so a drag leaves a sweep behind
     * the finger instead of the whole block pulsing at once. */
    animation: pick-glow 260ms ease-out;
}

@keyframes pick-glow {
    from {
        box-shadow: inset 0 0 0 2px var(--picked), 0 0 0 6px var(--picked-bg);
        transform: scale(1.04);
    }
    to {
        box-shadow: inset 0 0 0 2px var(--picked), 0 0 0 0 transparent;
        transform: scale(1);
    }
}

/* A markable square announces itself, or nobody discovers that opening it
 * offers anything. A dotted underline on the number is enough — the grid is
 * dense, and a second badge per square is not. */
.calendar .day[data-markable] .num {
    text-decoration: underline dotted var(--accent);
    text-underline-offset: 0.2em;
}

/* An admin's per-day count. Pushed to the bottom of the cell so the column of
 * figures lines up down the week regardless of what else each day holds. */
.calendar .day .note.attended {
    margin-top: auto;
    font-weight: 600;
    color: var(--accent);
}

/* The square is a button as far as the keyboard is concerned, so it needs a
 * focus ring that is not the browser's default blue. Handled by the shared
 * `:focus-visible` rule; this only lifts it above its neighbours. */
.calendar .day:focus-visible { z-index: 4; }

/* Rendered inside every square and never shown there — the card clones it. */
/* Nothing here hides the detail any more, and that is deliberate. It used to
 * carry `display: none`, from when a separate card cloned it out of the square;
 * now the square itself expands and shows its own detail in place, so the only
 * thing hiding it is the `hidden` attribute the script clears — which is exactly
 * what `[hidden]` at the top of this file already handles.
 *
 * A `display: none` here would beat that attribute being removed, and the square
 * would grow to a panel with nothing in it. */

/* --------------------------------------------------------------------------
 * Bigger squares on a phone
 *
 * The desktop grid is dense because a mouse can hit a 20-pixel target. A thumb
 * cannot, and this application is opened on a phone by most of the office —
 * so below this width the cells get taller, the type gets larger, and the
 * decorations that make a cell crowded are dropped rather than shrunk.
 * ------------------------------------------------------------------------ */

@media (max-width: 40rem) {
    /* The header first, because on a phone it was costing more screen than the
     * thing it sits above: eight nav links wrapping one per line pushed the
     * first row of the calendar below the fold. One scrollable row instead —
     * the links stay reachable, and the calendar starts where it should. */
    .site-header { padding: 0.5rem 0.75rem; gap: 0.5rem 0.75rem; }

    .site-nav {
        order: 5;
        flex-basis: 100%;
        flex-wrap: nowrap;
        overflow-x: auto;
        gap: 1rem;
        /* The bar is the navigation; a scrollbar under it is noise on a device
         * that scrolls by touch anyway. */
        scrollbar-width: none;
        -webkit-overflow-scrolling: touch;
    }

    .site-nav::-webkit-scrollbar { display: none; }

    /* Pills cost more height than the plain words they replaced, which on a
     * phone comes straight out of the calendar below. Tightened rather than
     * abandoned: the border is what makes each one a target, and that is the
     * point of them. */
    .site-nav a {
        white-space: nowrap;
        padding: 0.25rem 0.7rem;
        font-size: 0.875rem;
    }

    /* The month bar is the next thing between the header and the first week. */
    .month-nav { margin-bottom: 0.6rem; }
    .viewing-note { margin-bottom: 0.6rem; font-size: 0.875rem; }

    .brand { flex: 1; }

    /* The name goes, the avatar stays. This replaces a blunt
     * `.whoami { display: none }` that hid the identity altogether — on the
     * device most of the office actually uses, the header could not say which
     * account was signed in. The avatar costs about 28px and answers it. */
    .user-chip-name,
    .user-chip-caret { display: none; }

    .user-chip {
        padding: 0.2rem;
        /* Round, because with the name gone it is an avatar in a ring rather
         * than a pill with something missing from it. */
        border-radius: 50%;
    }

    /* Pushed to the right edge of whichever wrapped row it lands on.
     *
     * The header is one wrapping flex line, so without this the avatar sits
     * immediately after the notifications link, stranded mid-row with empty
     * space beside it. `margin-left: auto` eats that space, which puts the
     * account control in the corner people already reach for it in — and it
     * survives the row breaking differently as the nav gains or loses links,
     * where a fixed order or a spacer element would not. */
    .user-menu { margin-left: auto; }

    .calendar { gap: 3px; }

    .calendar .day {
        min-height: 4.6rem;
        padding: 0.5rem 0.35rem;
        font-size: 0.9375rem;
    }

    .calendar .day .num { font-size: 1.0625rem; }

    /* The holiday name and the "Owes 0.5" line are the first things to go: at
     * this width they wrap to three lines and push the number out of sight. The
     * card says both in full the moment the square is opened. */
    .calendar .day .note:not(.attended) { display: none; }

    .calendar .day .badge {
        font-size: 0.5625rem;
        padding: 0.05rem 0.25rem;
    }

    .page { padding: 1rem 0.75rem 4rem; }
}

/* --------------------------------------------------------------------------
 * The month, edge to edge
 *
 * The whole window below the header is the calendar. Full width, full height,
 * nothing to scroll: seven columns across the glass and however many weeks the
 * month has dividing the height between them.
 *
 * Two separate things had to change to get there, and they are worth telling
 * apart because only one of them is about height.
 *
 * **Height.** The rows used to be `min-height: clamp(6rem, 9.5vw, 10rem)`, which
 * on a wide monitor is 10rem — six of them being 960px of grid before the header
 * and the month bar are counted, so the last week of every month sat below the
 * fold. Rows now get a *share* of what is left rather than a height of their own.
 * `grid-auto-rows` rather than six explicit rows, because a month is four, five
 * or six weeks and six fixed rows would leave February a sixth of the grid as an
 * empty band.
 *
 * **Width.** `.page` carries `margin: 0 auto` to centre the readable column on
 * every other screen. That margin is why this page is a flex column and not
 * simply `height: 100dvh` — auto margins on a flex item's cross axis *absorb the
 * free space* instead of letting the item stretch, so leaving them in place
 * collapses the grid to a fit-content column stranded in the middle of the
 * window. `margin-inline: 0` is not tidying; it is the line that makes this full
 * width at all.
 *
 * The gutters go too. `.page-wide` kept 1rem at each side and 4rem at the foot,
 * which is right for a page that scrolls and wrong for one that is exactly the
 * size of the window: those are pixels the squares should have. The month bar
 * keeps its own inset so the heading is not jammed against the glass.
 *
 * **Above the phone breakpoint only.** On a phone the squares are already barely
 * a thumb wide, and squeezing six rows into what is left of a portrait screen
 * after the header would make each one shorter than the finger aiming at it. The
 * phone rules above stay exactly as they were: taller squares, and a scroll.
 *
 * `:has()` scopes all of this to the one page that has a calendar on it, so no
 * other screen is turned into a fixed-height column.
 * ------------------------------------------------------------------------ */

@media (min-width: 40.0625rem) {
    body:has(.calendar[data-calendar]) {
        /* `dvh`, not `vh`: on a tablet with a retracting browser chrome, `vh` is
         * the *largest* the viewport gets, which would put the last week under
         * the address bar until somebody scrolled — the exact problem this is
         * here to remove. */
        height: 100dvh;
        display: flex;
        flex-direction: column;
        /* The page is exactly the window now, so anything that overflows is a
         * rounding error rather than content somebody needs to reach. Without
         * this a stray pixel raises a scrollbar on a page whose whole point is
         * not having one. */
        overflow: hidden;
    }

    body:has(.calendar[data-calendar]) .page {
        /* The width fix, and the reason this rule exists at all. See above:
         * `margin: 0 auto` on a flex item stops it stretching and shrinks it to
         * its content, which is what left the grid marooned mid-window. */
        margin-inline: 0;
        width: 100%;
        max-width: none;

        /* `min-height: 0` is the equivalent load-bearing line for height. A flex
         * item's floor is its content by default, so without this the grid would
         * refuse to shrink below its natural height and the column would overflow
         * instead — the same scroll, arrived at by a longer route. */
        flex: 1 1 auto;
        min-height: 0;
        display: flex;
        flex-direction: column;

        /* Edge to edge. The side gutters and the 4rem foot are what a scrolling
         * page needs; this one is the size of the window. */
        padding: 0;
    }

    /* Everything above the grid keeps its natural height. Without this they are
     * shrinkable flex items like any other, so a tall month would take its
     * missing pixels partly out of the month heading and the messages — and the
     * grid is the only thing here that should be giving any up. */
    body:has(.calendar[data-calendar]) .page > *:not(.calendar) {
        flex: 0 0 auto;
    }

    /* The grid is flush with the glass; the words above it are not. Restoring an
     * inset on just these keeps "March 2026" and any message off the very edge
     * without costing the calendar a pixel. */
    body:has(.calendar[data-calendar]) .month-nav,
    body:has(.calendar[data-calendar]) .messages,
    body:has(.calendar[data-calendar]) .viewing-note {
        margin-inline: 1rem;
    }

    /* Tightened, because every row of it comes straight out of the squares. */
    body:has(.calendar[data-calendar]) .month-nav {
        margin-top: 0.75rem;
        margin-bottom: 0.75rem;
    }

    body:has(.calendar[data-calendar]) .calendar {
        flex: 1 1 auto;
        /* For the same reason as on `.page`: the grid is itself a flex item. */
        min-height: 0;

        /* The weekday headings keep their own height; every week row splits what
         * is left. `minmax(0, 1fr)` rather than plain `1fr` because a grid row's
         * default floor is also its content, and one cell with a long holiday
         * name would otherwise push its whole row taller than its siblings. */
        grid-template-rows: auto;
        grid-auto-rows: minmax(0, 1fr);

        /* Rounded corners and side borders are for a card sitting on a page.
         * This is not sitting on anything — it runs into the edges of the
         * window, and a radius there reads as a rendering fault. */
        border-radius: 0;
        border-left: 0;
        border-right: 0;
        border-bottom: 0;
    }

    body:has(.calendar[data-calendar]) .calendar .day {
        /* Replaces the clamp. The row height is the grid's decision now. */
        min-height: 0;
        /* Insurance, for the shortest rows a six-week month on a small laptop
         * produces: a cell clips its own contents rather than growing and
         * dragging the rest of the week with it. The expanded panel is
         * `position: fixed` — set by `calendar.js` — so it is not affected by
         * this, and neither is the lift on press, which is a transform. */
        overflow: hidden;
    }

    /* The blanks are ordinary rows now, not the 3rem stubs they were when every
     * row was sized independently. Left at their own height they would compress
     * the first and last weeks of the month against the others. */
    body:has(.calendar[data-calendar]) .calendar .day.blank { min-height: 0; }
}

/* --------------------------------------------------------------------------
 * Reading somebody else's month
 * ------------------------------------------------------------------------ */

/* Every button in the grid below writes against their name, not yours, so this
 * is a state worth being unable to miss. */
.viewing-note {
    margin: 0 0 1rem;
    padding: 0.6rem 0.9rem;
    border-left: 4px solid var(--accent);
    border-radius: var(--radius);
    background: var(--surface);
    color: var(--muted);
    font-size: 0.9375rem;
}

/* --------------------------------------------------------------------------
 * The square that becomes the panel
 *
 * There is no separate popup. Tapping a day promotes that square: it lifts out
 * of the grid, grows over the calendar, and shows the detail already inside it.
 * `calendar.js` measures and pins; everything here is what that looks like.
 *
 * The geometry is set inline by the script, so these rules deliberately set no
 * width, height, left or top — only what the transition animates and what the
 * expanded state looks like.
 * ------------------------------------------------------------------------ */

.card-scrim {
    position: fixed;
    inset: 0;
    background: rgba(20, 10, 18, 0.45);
    opacity: 0;
    transition: opacity 180ms ease-out;
    z-index: 20;
}

.card-scrim.is-open { opacity: 1; }

/* The square while it is pinned, whether growing or shrinking. `position` and
 * the four offsets come from the script; this is the part that has to be true
 * for the whole of the animation. */
.calendar .day.is-expanding {
    z-index: 30;
    overflow: hidden;
    border-radius: calc(var(--radius) * 2);
    box-shadow: 0 18px 50px rgba(0, 0, 0, 0.32);
    transition:
        left 260ms cubic-bezier(0.22, 0.9, 0.25, 1),
        top 260ms cubic-bezier(0.22, 0.9, 0.25, 1),
        width 260ms cubic-bezier(0.22, 0.9, 0.25, 1),
        height 260ms cubic-bezier(0.22, 0.9, 0.25, 1);
}

/* Once it has arrived. The square's own summary — the number, the badges, the
 * count — is what it showed as a grid cell, and the detail replaces it rather
 * than sitting under it. */
.calendar .day.is-expanded {
    background: var(--surface);
    padding: 0;
    overflow-y: auto;
    cursor: default;
}

.calendar .day.is-expanded > .num,
.calendar .day.is-expanded > .note,
.calendar .day.is-expanded > .badge { display: none; }

/* What holds the square's place in the grid while it is out of the flow.
 * Without it the remaining days reflow into the gap and the animation appears
 * to start from somewhere else entirely. */
.calendar .day-placeholder {
    background: var(--bg);
    pointer-events: none;
}

.day-detail {
    padding: 1.1rem 1.25rem 1.35rem;
    /* Fades in behind the growing square rather than appearing at full size in
     * a cell that is still the size of a calendar day. */
    animation: detail-in 200ms ease-out both;
    animation-delay: 60ms;
}

@keyframes detail-in {
    from { opacity: 0; transform: translateY(4px); }
    to { opacity: 1; transform: none; }
}

.card-close {
    position: absolute;
    top: 0.35rem;
    right: 0.35rem;
    border: none;
    background: none;
    color: var(--muted);
    font-size: 1.5rem;
    line-height: 1;
    padding: 0.25rem 0.5rem;
    cursor: pointer;
    z-index: 1;
}

.card-close:hover { color: var(--text); }

.card-title { font-size: 1.0625rem; font-weight: 600; margin: 0 1.5rem 0.5rem 0; }
.card-subtitle {
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    font-weight: 600;
    color: var(--muted);
    margin: 1.15rem 0 0.5rem;
}
.card-line { margin: 0 0 0.6rem; font-size: 0.9375rem; }

/* Plan §6.1's three buttons, and the same three when an admin is correcting.
 * Side by side and full width, because this is what nineteen people out of
 * twenty open the application to press. */
.card-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    margin: 0.6rem 0 0.25rem;
}

.card-actions .day-action {
    flex: 1 1 6rem;
    padding: 0.7rem 0.4rem;
    font-weight: 500;
    font-size: 0.9375rem;
}

.card-actions .day-action.is-current {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--accent-text);
}

/* An admin's correction, marked out from ordinary marking because it writes
 * against somebody else's name and plan §5.5 makes the reason mandatory. */
.card-correct {
    border-left: 3px solid var(--accent);
    padding-left: 0.75rem;
    margin-top: 0.75rem;
}

.card-correct textarea { max-width: none; }

/* --------------------------------------------------------------------------
 * Who was in
 *
 * Pills rather than raw links. Each one is also the way into that person's
 * month, which is how another calendar is reached now that the dropdown is
 * gone — so it has to look like something you can press.
 * ------------------------------------------------------------------------ */

.attender-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
}

.attender-pill {
    display: inline-flex;
    align-items: baseline;
    gap: 0.4rem;
    padding: 0.35rem 0.7rem;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--bg);
    color: var(--text);
    text-decoration: none;
    font-size: 0.875rem;
    transition: border-color 120ms ease-out, background 120ms ease-out;
}

.attender-pill:hover {
    border-color: var(--accent);
    background: var(--surface);
    color: var(--text);
}

/* The status tints the pill's edge, so a half day is visible without reading
 * every label — and the label is still there, which is what the palette's
 * colour-blind rule requires. */
.attender-pill.is-present { border-left: 3px solid var(--present); }
.attender-pill.is-half_day { border-left: 3px solid var(--half); }

.attender-name { font-weight: 500; }
.attender-code { color: var(--muted); font-size: 0.75rem; font-variant-numeric: tabular-nums; }
.attender-status { color: var(--muted); font-size: 0.75rem; }

/* The forms host, which the script moves into whichever square is open. */
.card-forms { margin-top: 0.5rem; }

/* The page does not scroll behind an open square. Without this a scroll inside
 * the panel that reaches its end starts moving the calendar underneath — and
 * the square is pinned to viewport coordinates, so the two would come apart. */
body.card-open { overflow: hidden; }

/* --------------------------------------------------------------------------
 * Reduced motion
 *
 * Every animation above is decoration on something that already works. Somebody
 * who has asked their system for less movement gets the same calendar with the
 * lift, the glow and the slide removed rather than a degraded one.
 * ------------------------------------------------------------------------ */

@media (prefers-reduced-motion: reduce) {
    .calendar .day,
    .calendar .day.is-expanding,
    .card-scrim {
        transition: none;
    }

    .day-detail { animation: none; }

    .calendar .day.is-picked { animation: none; }
    .calendar .day.is-pressed { transform: none; }

    /* It still appears, it just stops sliding. A warning that animates in is the
     * one piece of motion here somebody cannot avoid by not touching anything. */
    .connection-toast { animation: none; }
}

/* --------------------------------------------------------------------------
 * Dashboard figures
 * ------------------------------------------------------------------------ */

.stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr));
    gap: 0.75rem;
}

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

.stat .label {
    display: block;
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.02em;
    color: var(--muted);
}

.stat .value {
    display: block;
    font-size: 1.5rem;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}

.stat .value.small { font-size: 1.125rem; }

.locked-note {
    display: inline-block;
    font-size: 0.8125rem;
    color: var(--muted);
    border: 1px dashed var(--border);
    border-radius: var(--radius);
    padding: 0.2rem 0.5rem;
}

/* --------------------------------------------------------------------------
 * Sign-in
 * ------------------------------------------------------------------------ */

.signin {
    max-width: 22rem;
    margin: 4rem auto;
}

/* --------------------------------------------------------------------------
 * Notifications (#62)
 *
 * The badge is server-rendered with the count as it stood when the page was
 * built, then refreshed by polling. It is therefore correct with scripting off
 * and merely stops updating — which is why `is-empty` is a class the stylesheet
 * interprets rather than a style the script sets.
 * ------------------------------------------------------------------------ */

.notif-link {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    color: var(--muted);
    text-decoration: none;
    font-size: 0.875rem;
}

.notif-link:hover { color: var(--accent); }

.notif-badge {
    display: inline-block;
    min-width: 1.4rem;
    padding: 0.05rem 0.4rem;
    border-radius: 999px;
    background: var(--accent);
    color: var(--accent-text);
    font-size: 0.75rem;
    font-weight: 600;
    text-align: center;
    font-variant-numeric: tabular-nums;
}

/* Zero unread. Kept in the layout rather than hidden, so the header does not
 * reflow every time the count crosses zero — a row of links that shifts sideways
 * while somebody is aiming at one is worse than a grey nought. */
.notif-badge.is-empty {
    background: transparent;
    color: var(--muted);
    border: 1px solid var(--border);
}

/* Before the first poll answers, and forever where scripting is off. `display:
 * none` rather than `visibility: hidden`: there is no number yet, so reserving
 * space for one would leave a gap that never fills. */
.notif-badge.is-hidden { display: none; }

/* The poll is failing, so the number is stale.
 *
 * Drained of colour rather than hidden or blanked. What it shows was true when
 * it arrived and is only no longer known to be current — hiding it would claim
 * more than that, and emptying it would move every link in the header sideways
 * for a problem that may last one poll. Same size, same position, less
 * insistence; the `title` the script sets says the rest.
 *
 * After `.is-empty` so a stale zero reads as stale rather than as empty: both
 * are single-class selectors, so this only wins by coming later. */
.notif-badge.is-stale {
    background: transparent;
    color: var(--muted);
    border: 1px dashed var(--border);
}

/* --------------------------------------------------------------------------
 * The connection toast
 *
 * What the page says when the badge poll stops working — the wording lives in
 * `partials/connection_toast.html`, because a `.po` catalogue cannot reach a
 * string inside a script.
 *
 * `position: fixed`, and that is a requirement rather than a preference: the
 * calendar is a fixed-height flex column filling the viewport exactly, so
 * anything in normal flow would take its height out of the week rows and
 * reintroduce the scroll that layout exists to remove.
 * ------------------------------------------------------------------------ */

.connection-toast {
    position: fixed;
    /* Bottom rather than top: the header is already the busiest part of the
     * screen, and on the calendar the top edge holds the month navigation. */
    bottom: 1rem;
    right: 1rem;
    /* Clear of everything the calendar puts on screen — the scrim is 20, an
     * expanded day is 30, and the account menu is 50. Being told you are signed
     * out matters more than the panel you are being told it over. */
    z-index: 60;

    display: flex;
    align-items: center;
    gap: 0.75rem;
    max-width: min(28rem, calc(100vw - 2rem));
    padding: 0.75rem 0.9rem;

    /* The warning palette from `.message-warning`, so this reads as the same
     * class of thing as a server-rendered warning rather than a new vocabulary. */
    background: var(--half-bg);
    border: 1px solid var(--half);
    border-radius: var(--radius);
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.22);
    font-size: 0.875rem;

    animation: toast-in 180ms ease-out both;
}

@keyframes toast-in {
    from { opacity: 0; transform: translateY(0.5rem); }
    to { opacity: 1; transform: none; }
}

.connection-toast-text { margin: 0; }

/* The one thing that fixes the signed-out case, so it looks pressable. */
.connection-toast-action {
    color: var(--accent);
    font-weight: 600;
    white-space: nowrap;
}

.connection-toast-close {
    border: none;
    background: none;
    color: var(--muted);
    font-size: 1.25rem;
    line-height: 1;
    padding: 0.1rem 0.25rem;
    margin-left: auto;
    cursor: pointer;
}

.connection-toast-close:hover { color: var(--text); }

.notification-list { list-style: none; margin: 0; padding: 0; }

.notification {
    background: var(--surface);
    border: 1px solid var(--border);
    border-left: 4px solid var(--border);
    border-radius: var(--radius);
    /* No padding here any more. It moved onto the link inside, so the padded
     * area is part of the target rather than a dead margin around it — a row
     * whose edges do not respond is a row people click twice. */
    margin-bottom: 0.6rem;
    /* The link's own focus ring and hover fill are drawn inside these corners. */
    overflow: hidden;
}

/* The whole row, as one target.
 *
 * `display: block` and `color: inherit` are what stop this looking like a link
 * and start it looking like the row it replaced: a list of blue underlined
 * titles would read as a page of links rather than as an inbox. What marks it
 * as clickable is that it responds — the fill on hover, the ring on focus. */
.notification-link {
    display: block;
    padding: 0.9rem 1rem;
    color: inherit;
    text-decoration: none;
}

.notification-link:hover { background: var(--bg); }

/* Inset, because the row clips its own corners: an outline drawn outside the
 * link would be shaved off by the `overflow: hidden` above. */
.notification-link:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: -2px;
}

/* The title is the thing being clicked, so it is what underlines — not the
 * whole row, and not the timestamp. */
.notification-link:hover .notification-title { text-decoration: underline; }

/* Unread is marked by a coloured edge as well as by weight. Colour alone would
 * fail the same test the status palette above is built against: somebody who
 * cannot distinguish the two must still be able to tell which are new. */
.notification.is-unread {
    border-left-color: var(--accent);
    background: var(--surface);
}

.notification.is-unread .notification-title { font-weight: 600; }

.notification-head {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 0.75rem;
    color: var(--muted);
    font-size: 0.75rem;
    margin-bottom: 0.35rem;
}

.notification-title { margin: 0; }

.notification-body {
    color: var(--muted);
    font-size: 0.875rem;
}

.notification-body p { margin: 0.4rem 0 0; }

/* One badge colour per notification kind, matching the status palette where the
 * meanings line up — a record an admin changed is the same concern as an absent
 * day, and an overdue request is the one thing on this list that is overdue. */
.badge-attendance_reminder { color: var(--half); }
.badge-request_submitted { color: var(--accent); }
.badge-request_decided { color: var(--present); }
.badge-record_changed { color: var(--absent); }
.badge-calendar_changed { color: var(--muted); }
.badge-monthly_summary { color: var(--leave); }
.badge-request_overdue { color: var(--absent); }

/* --------------------------------------------------------------------------
 * Notices — 403, 404, and the overview's call to action
 * ------------------------------------------------------------------------ */

.notice { max-width: 40rem; }
.notice h1 { margin-bottom: 0.75rem; }
.notice p { margin: 0 0 1rem; }

/* The 403 is reached by a real refusal, so it is coloured like one. The
 * overview's employment prompt is an invitation rather than a rejection, which
 * is why it gets the accent and not the red. */
.notice-denied { border-left: 4px solid var(--absent); }
.notice-error { border-left: 4px solid var(--absent); }
.notice-action { border-left: 4px solid var(--accent); }

/* --------------------------------------------------------------------------
 * Overview and employee list
 * ------------------------------------------------------------------------ */

/* A non-zero overdue count is the one number on the overview that means
 * something is wrong rather than merely being large. */
.stat .value.is-alarming { color: var(--absent); }

.muted { color: var(--muted); }

/* A former employee's row stays legible rather than being greyed to the point of
 * being hard to read — their history is still the answer to "who was AR-...?",
 * which is precisely why the row is there at all. */
.is-former td { color: var(--muted); }

.month-actions { display: flow-root; }
.month-actions form { margin-bottom: 0.5rem; }
.month-actions .helptext { margin: 0; }
