/* =============================================================================
   theme.css - Your theme. Sits on top of style.css (never edit that file).
   =============================================================================
   Simple configuration for the app's look: fonts, brand color, dark mode.
   Loaded after style.css, so anything here wins. Project-level component
   styles belong here too; feature styles (addons) ship as their own CSS
   files under includes/css/.
   ============================================================================= */

/* -----------------------------------------------------------------------------
   1. CONFIG - change these to re-skin the app
   -------------------------------------------------------------------------- */

:root {
    /* Fonts. Inter is loaded from Google Fonts by the <link> tags in
       index.html (weights 400-700, variable); the system stack behind it
       shows until it arrives and stands in wherever the font is blocked.
       To change the face, swap the family here and the family in the link. */
    --headings-font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, 'Helvetica Neue', Helvetica, sans-serif;
    --body-font:     'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, 'Helvetica Neue', Helvetica, sans-serif;

    /* Brand - ink by default: buttons and accents follow the text color and
       invert automatically in dark mode. For a colored brand, point these at
       any style.css palette color instead (e.g. var(--violet) / var(--violet-rgb))
       and the tints, focus rings, and active nav states all follow. Change it
       in these :root values AND in the three override blocks below
       (body.dark, the prefers-color-scheme query, body.light). */
    --brand:     var(--black);
    --brand-rgb: var(--black-rgb);

    /* Shape & shell */
    --radius:        8px;    /* controls: inputs, buttons, pills */
    --radius-lg:     12px;   /* surfaces: cards, tables */
    --sidebar-width: 256px;

    /* Floating buttons: the size of each round button, the space between
       two of them and their distance from the screen edges. The footer band
       under the stack is sized from the same three values. */
    --fab-size:   48px;
    --fab-gap:    12px;
    --fab-offset: 16px;

    /* Modals (ui.js): the widest a modal grows, its corners, its shadow and
       the page behind it. The defaults are the cards' rounded corners, a
       small shadow and a dimmed page with no blur; the modal fades in. A
       project restyles every modal by setting these in its own :root;
       anything further (a gradient, a different entrance) goes in its own
       .modal rules. */
    --modal-width:    480px;
    --modal-radius:   var(--radius-lg);
    --modal-shadow:   0 12px 32px -8px rgba(var(--shadow-rgb), .24);
    --modal-backdrop: rgba(var(--shadow-rgb), .5);
    --modal-blur:     0px;

    /* Hairline used for every border in the app; flips with the mode */
    --border: rgba(var(--black-rgb), .12);

    /* Tokens that never flip: text on a coloured badge or button (it stays
       white in both modes), the ink of every shadow (shadows darken in both
       modes), and the amber and green surfaces that carry --on-accent text.
       The style.css amber and green are too light for white text, so these
       are their darker shades, which hold it at 5:1. */
    --on-accent:    #FFFFFF;
    --shadow-rgb:   0, 0, 0;
    --amber-strong: #B45309;
    --green-strong: #15803D;


    /* Error messages and the edge of an invalid control. Flips with the
       mode: a deep red on the light surfaces, a soft red on the dark ones,
       both above 4.5:1 on the canvas, the cards and the fields. */
    --error-text: #B91C1C;

    /* Plate behind the brand_logos images. Logo artwork is drawn in dark ink
       on a transparent background, so it reads straight off the surface in
       light mode and needs a white card under it in dark mode to stay
       legible. A neutral token cannot do this: they all flip. */
    --logo-plate: transparent;

    /* The two surfaces that are not plain cards: the chrome (top bar and
       sidebar) and the fields (inputs, selects, textareas). Both modes layer
       the shell like a code editor - chrome, then the canvas, the cards, the
       fields - each one step lighter in light mode and darker in dark mode. */
    --chrome: #F3F3F3;
    --field:  #FFFFFF;

    /* Neutrals of a code editor's light theme replacing the style.css
       defaults: soft #3B3B3B ink instead of jet black, an #F8F8F8 canvas,
       pure white cards, #E5E5E5 dividers */
    --black:  #3B3B3B;   --black-rgb:  59, 59, 59;
    --white:  #FFFFFF;   --white-rgb:  255, 255, 255;
    --dark:   #505050;   --dark-rgb:   80, 80, 80;
    --gray:   #616161;   --gray-rgb:   97, 97, 97;
    --silver: #E5E5E5;   --silver-rgb: 229, 229, 229;
    --light:  #F8F8F8;   --light-rgb:  248, 248, 248;
}

/* Dark mode - the layered grays of a code editor's dark theme rather than a
   flat near-black: #181818 chrome (top bar, sidebar), a #1F1F1F canvas,
   #252526 cards, #313131 fields, #2B2B2B dividers and #CCCCCC text, so
   every layer is told apart at a glance and buttons invert to light. Keep
   both blocks in sync: body.dark is the in-app toggle, the media query is
   the OS preference. The derived tokens (--brand, --border) are repeated in
   the body.* blocks because var() inside a custom property resolves where
   the property is declared - :root - so they must be re-declared on body to
   pick up the flipped neutrals. */
body.dark {
    --black:  #CCCCCC;   --black-rgb:  204, 204, 204;
    --white:  #252526;   --white-rgb:  37, 37, 38;
    --dark:   #B4B4B4;   --dark-rgb:   180, 180, 180;
    --gray:   #9D9D9D;   --gray-rgb:   157, 157, 157;
    --silver: #2B2B2B;   --silver-rgb: 43, 43, 43;
    --light:  #1F1F1F;   --light-rgb:  31, 31, 31;

    --brand:     var(--black);
    --brand-rgb: var(--black-rgb);
    --border:    rgba(var(--black-rgb), .10);

    --chrome: #181818;
    --field:  #313131;

    --logo-plate: #FFFFFF;

    --error-text: #F87171;
}

@media (prefers-color-scheme: dark) {
    :root {
        --black:  #CCCCCC;   --black-rgb:  204, 204, 204;
        --white:  #252526;   --white-rgb:  37, 37, 38;
        --dark:   #B4B4B4;   --dark-rgb:   180, 180, 180;
        --gray:   #9D9D9D;   --gray-rgb:   157, 157, 157;
        --silver: #2B2B2B;   --silver-rgb: 43, 43, 43;
        --light:  #1F1F1F;   --light-rgb:  31, 31, 31;

        --brand:     var(--black);
        --brand-rgb: var(--black-rgb);
        --border:    rgba(var(--black-rgb), .10);

        --chrome: #181818;
        --field:  #313131;

        --logo-plate: #FFFFFF;

        --error-text: #F87171;
    }
}

/* Explicit light mode - the in-app toggle sets body.light to override a
   dark OS preference. Values mirror the :root defaults above; this is the
   one sanctioned place neutrals are written outside the config block. */
body.light {
    --black:  #3B3B3B;   --black-rgb:  59, 59, 59;
    --white:  #FFFFFF;   --white-rgb:  255, 255, 255;
    --dark:   #505050;   --dark-rgb:   80, 80, 80;
    --gray:   #616161;   --gray-rgb:   97, 97, 97;
    --silver: #E5E5E5;   --silver-rgb: 229, 229, 229;
    --light:  #F8F8F8;   --light-rgb:  248, 248, 248;

    --brand:     var(--black);
    --brand-rgb: var(--black-rgb);
    --border:    rgba(var(--black-rgb), .12);

    --chrome: #F3F3F3;
    --field:  #FFFFFF;

    --logo-plate: transparent;

    --error-text: #B91C1C;
}

/* -----------------------------------------------------------------------------
   2. TYPOGRAPHY - tighter tracking on large text reads more refined
   -------------------------------------------------------------------------- */

h1, h2, h3 {
    letter-spacing: -0.01em;
}

/* A word longer than the line (an email address in the dashboard greeting,
   a pasted link) breaks instead of pushing the page wider than the screen */
h1, h2, h3, h4, h5, h6,
p {
    overflow-wrap: anywhere;
}

/* Inter is drawn for screens: grayscale smoothing on macOS keeps its strokes
   at the weight they were designed at, and the variable axis is left to the
   browser so every weight between 400 and 700 renders from one file */
body {
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    font-optical-sizing: auto;
}

/* Vertical rhythm: text blocks keep comfortable spacing everywhere
   (style.css only gives them 8px) */
h1, h2, h3, h4, h5, h6 {
    margin-bottom: 16px;
}

p {
    margin-bottom: 12px;
}

/* -----------------------------------------------------------------------------
   3. BUTTONS - solid ink, hairline border, layered hover instead of the
   style.css opacity fade
   -------------------------------------------------------------------------- */

button,
.button {
    background: var(--brand);
    border: 1px solid var(--brand);
    color: var(--white); /* inverts with --brand, so it works in both modes */
    padding: 8px 16px;
    border-radius: var(--radius);
    letter-spacing: .01em;
    transition: background-color .15s ease, border-color .15s ease;
}

/* Hover: a translucent --white veil lightens the ink button in light mode
   and dims the inverted button in dark mode - one rule for both */
button:not([disabled]):hover,
.button:not([disabled]):hover {
    opacity: 1;
    background-image: linear-gradient(rgba(var(--white-rgb), .14), rgba(var(--white-rgb), .14));
}

button:focus-visible,
.button:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: 2px;
}

/* Secondary: quiet surface button with a hairline edge */
button.button-alt,
.button.button-alt {
    background: var(--white);
    border-color: rgba(var(--black-rgb), .15);
    color: var(--black);
}

button.button-alt:not([disabled]):hover,
.button.button-alt:not([disabled]):hover {
    background-image: linear-gradient(rgba(var(--black-rgb), .04), rgba(var(--black-rgb), .04));
    border-color: rgba(var(--black-rgb), .3);
}

/* Destructive: the red of a delete, used by the confirm box of a
   destructive action (confirmDialog() with danger) and the Delete button of
   a detail page. Hover darkens the red in both modes (a --white veil would
   lighten it in light mode and weaken the white label), and the focus ring
   is red too. */
button.button-danger,
.button.button-danger {
    background: var(--red);
    border-color: var(--red);
    color: var(--on-accent);
}

button.button-danger:not([disabled]):hover,
.button.button-danger:not([disabled]):hover {
    background-image: linear-gradient(rgba(var(--shadow-rgb), .16), rgba(var(--shadow-rgb), .16));
}

button.button-danger:focus-visible,
.button.button-danger:focus-visible {
    outline-color: var(--red);
}

/* -----------------------------------------------------------------------------
   4. CORE UI - small shared pieces every project uses
   -------------------------------------------------------------------------- */

/* Badge shown in the header whenever the app runs in demo mode */
.demo-badge {
    display: inline-block;
    padding: 0 8px;
    line-height: 20px;
    margin-inline-start: 4px;
    border-radius: 4px;
    background: var(--amber-strong);
    color: var(--on-accent);
    font-size: 0.75rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    vertical-align: middle;
}

/* Heading block at the top of the dashboard and other core pages */
/* The title labels the screen once and then steps back behind a hairline */
.page-header {
    margin-bottom: 24px;
    padding-bottom: 16px;
    border-bottom: 1px solid var(--border);
}

/* -----------------------------------------------------------------------------
   5. SHELLS - one header markup, two looks.
   Public pages (login, register, landing pages): the style.css top bar.
   App pages: getHeaderHTML() sets the app-shell class on <body> when the
   user is authenticated. On desktop the header dissolves into the canvas
   (just the utility links, top end) while its sidebar-panel column (logo +
   menus) is fixed to the left. Mobile always keeps the top bar and
   expandable menu; the menu inside it uses the same pills as the sidebar
   and ends with the utility links.
   -------------------------------------------------------------------------- */

/* Soft canvas behind the content; cards sit on --white and the chrome (top
   bar, sidebar) on --chrome. All three flip in dark mode, so this needs no
   dark overrides. */
body {
    background: var(--light);
}

.header {
    background: var(--chrome);
}

/* Below 768px style.css folds the menu columns to max-height 0, which
   hides them from the eye but not from the Tab key. Folded, they are also
   hidden from the keyboard and screen readers: visibility turns hidden once
   the closing slide has run and visible the moment the menu opens. From
   768px the columns are always shown. */
.header .custom-column.hidden-for-mobile {
    visibility: hidden;
    transition: max-height .25s ease, visibility 0s linear .25s;
}

.header.expanded .custom-column.hidden-for-mobile {
    visibility: visible;
    transition: max-height .25s ease, visibility 0s linear 0s;
}

@media (min-width: 768px) {
    .header .custom-column.hidden-for-mobile {
        visibility: visible;
    }
}

/* The site name as text in the header (getBrandHTML() without brand_logos):
   smaller than style.css's 1.75rem / 2.5rem, so the top bar stays slim */
.header a > p {
    font-size: 1.5rem;
}

@media (min-width: 768px) {
    .header a > p {
        font-size: 2rem;
    }
}

/* Brand lockup: the brand_logos images rendered by getBrandHTML(), sized off
   a shared height so they align on one baseline whatever their proportions,
   in the top bar and in the sidebar alike. Absent from projects that leave
   brand_logos empty and show the site name as text. */
.brand-lockup {
    display: inline-flex;
    align-items: center;
    padding: 4px 8px;
    border-radius: var(--radius);
    background: var(--logo-plate);
    vertical-align: middle;
}

.brand-lockup img {
    display: block;
    width: auto;
    height: 40px;
}

.brand-lockup img + img {
    margin-inline-start: 12px;
}

@media (min-width: 768px) {
    .brand-lockup img {
        height: 48px;
    }
}

/* Inside the app shell the sidebar already frames the content column, so
   containers drop the style.css max-width and span the full width: the top
   bar, pages, and footer all align to the same 16px gutter. */
.app-shell .custom-container {
    max-width: 100%;
}

/* The sidebar logo only exists on desktop; mobile uses the top-bar logo */
.sidebar-logo {
    display: none;
}

/* Authenticated mobile top bar: the mobile-logo column takes the same 70%
   slot the public logo column gets from style.css. The column only exists
   on mobile (hidden-for-desktop), so the rule needs no breakpoint. */
.header .custom-column.mobile-logo {
    flex: 0 0 70%;
    width: 70%;
    order: 0;
    text-align: start;
    overflow: hidden;
}

@media (min-width: 768px) {
    /* Push every page (top bar, content, and footer) clear of the sidebar.
       The page and #app are as tall as the window at least, and #app is a
       flex column, so the footer settles at the bottom edge of the screen
       on every page instead of trailing the content at a different height
       per page. */
    html,
    body.app-shell {
        height: 100%;
    }

    .app-shell #app {
        display: flex;
        flex-direction: column;
        min-height: 100%;
        padding-inline-start: var(--sidebar-width);
    }

    .app-shell .footer {
        margin-top: auto;
    }

    /* The top bar dissolves into the canvas: only the utility links remain,
       floating in the top end corner */
    .app-shell .header {
        background: transparent;
        border-bottom: 0;
        box-shadow: none;
    }

    .app-shell .header .custom-container {
        padding: 8px 16px;
    }

    .app-shell .header .custom-container > .custom-row {
        justify-content: flex-end;
    }

    /* Text brand (projects that leave brand_logos empty) */
    .app-shell .header a > p {
        font-size: 1.5rem;
    }

    /* Sidebar: logo + menu navigation, fixed to the start edge */
    .app-shell .sidebar-panel {
        position: fixed;
        top: 0;
        bottom: 0;
        inset-inline-start: 0;
        width: var(--sidebar-width);
        overflow-y: auto;
        padding: 24px 16px;
        background: var(--chrome);
        border-inline-end: 1px solid var(--border);
        font-size: .9rem;
        scrollbar-width: thin;
        scrollbar-color: rgba(var(--black-rgb), .15) transparent;
        z-index: 20;
    }

    .app-shell .sidebar-logo {
        display: inline-block;
        margin-bottom: 24px;
    }

}

/* Sidebar menu. These rules sit outside the media queries because the
   sidebar-panel column is the same markup at every width: the fixed panel
   on desktop and the expanded menu under the mobile top bar. Only the panel
   position above is desktop-only. */

/* The list bleeds 8px into the panel padding so pill text (8px inset)
   sits exactly on the 16px line, flush with the logo, while hover pills
   keep an 8px gutter to the panel edges */
.app-shell .sidebar-panel nav > ul {
    flex-direction: column;
    align-items: stretch;
    margin-inline: -8px;
}

.app-shell .sidebar-panel li {
    position: static;
    flex: 0 0 auto;
}

.app-shell .sidebar-panel li:not(:last-child) {
    margin: 0;
    margin-bottom: 4px;
}

/* Menu links become rounded pills. Hover is a quiet neutral wash;
   the active page gets the stronger brand tint and weight. */
.app-shell .sidebar-panel li a {
    display: block;
    padding: 8px;
    border-radius: var(--radius);
    color: var(--dark);
    font-weight: 500;
    transition: background .15s ease, color .15s ease;
}

.app-shell .sidebar-panel li a:hover {
    text-decoration: none;
    background: rgba(var(--black-rgb), .04);
    color: var(--black);
}

.app-shell .sidebar-panel li.active > a {
    text-decoration: none;
    background: rgba(var(--brand-rgb), .08);
    color: var(--brand);
    font-weight: 600;
}

/* Submenus render inline and indented instead of floating dropdowns;
   the 16px indent puts child link text one full step in from the
   parents' text (16px -> 32px from the sidebar edge) */
.app-shell .sidebar-panel li > ul {
    display: block;
    position: static;
    min-width: 0;
    margin: 4px 0;
    padding: 0;
    padding-inline-start: 16px;
    background: transparent;
    box-shadow: none;
    text-align: start;
}

.app-shell .sidebar-panel li > ul::before {
    display: none;
}

.app-shell .sidebar-panel li > ul > li {
    padding: 0;
}

/* Menu markers: a small dot in the pill's gutter gives every entry an
   anchor for the eye without icons. Quiet at rest, brand-coloured on the
   active page. Link text moves to 24px (8px pad, 6px dot, 10px gap) while
   captions and the switcher stay at 8px, in line with the dots; submenu
   children drop the dot, since their indent already sets them apart, and
   their text lands under the parent's text. */
.app-shell .sidebar-panel li a {
    position: relative;
    padding-inline-start: 24px;
}

.app-shell .sidebar-panel li a::before,
.app-shell .header .nav-group-label::before {
    content: '';
    position: absolute;
    inset-inline-start: 8px;
    top: calc(50% - 3px);
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: rgba(var(--black-rgb), .25);
    transition: background .15s ease;
}

.app-shell .sidebar-panel li a:hover::before,
.app-shell .header .nav-group-label:hover::before {
    background: var(--gray);
}

.app-shell .sidebar-panel li.active > a::before {
    background: var(--brand);
}

.app-shell .sidebar-panel li > ul > li a {
    padding-inline-start: 8px;
}

.app-shell .sidebar-panel li > ul > li a::before {
    display: none;
}

/* style.css caps the public expanded menu at 512px; the app menu grows
   with its sections and addons, so the cap is lifted (only the mobile menu
   ever expands) */
.app-shell .header.expanded .custom-column.hidden-for-mobile {
    max-height: none;
}

/* Mobile: the utility links continue the menu as its last block, set apart
   by a hairline, and take the same pill footprint and marker as the menu
   links */
.app-shell .header .sidebar-panel nav {
    padding-bottom: 0;
}

.app-shell .header .header-utility nav {
    margin-top: 12px;
    padding-top: 12px;
    border-top: 1px solid var(--border);
}

.app-shell .header-utility nav > ul {
    flex-direction: column;
    align-items: stretch;
    margin-inline: -8px;
}

.app-shell .header-utility li {
    flex: 0 0 auto;
}

.app-shell .header-utility li a {
    position: relative;
    display: block;
    padding: 8px;
    padding-inline-start: 24px;
    border-radius: var(--radius);
    color: var(--dark);
    font-weight: 500;
    transition: background .15s ease, color .15s ease;
}

.app-shell .header-utility li a:hover {
    text-decoration: none;
    background: rgba(var(--black-rgb), .04);
    color: var(--black);
}

.app-shell .header-utility li a::before {
    content: '';
    position: absolute;
    inset-inline-start: 8px;
    top: calc(50% - 3px);
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: rgba(var(--black-rgb), .25);
}

/* Desktop: the utility links (dark mode, logout) float in the top end
   corner as quiet pills, side by side, without the menu marker */
@media (min-width: 768px) {
    .app-shell .header .header-utility nav {
        margin-top: 0;
        padding-top: 0;
        border-top: 0;
    }

    .app-shell .header-utility nav > ul {
        flex-direction: row;
        align-items: center;
        margin-inline: 0;
    }

    .app-shell .header-utility li:not(:last-child) {
        margin: 0;
        margin-inline-end: 8px;
    }

    .app-shell .header-utility li a {
        padding: 8px 16px;
        color: var(--gray);
        font-size: .9rem;
    }

    .app-shell .header-utility li a:hover {
        background: rgba(var(--black-rgb), .05);
    }

    .app-shell .header-utility li a::before {
        display: none;
    }
}

/* Sidebar section captions: NAV_SECTIONS groups the menu links, and each
   group that has links starts with a small muted caption. The 8px inline
   padding puts the caption text on the same line as the pill text, and a
   caption after another group gets breathing room above. Outside the media
   query so the mobile expanded menu shows the same grouping. */
.app-shell .header li.nav-section-title {
    margin-top: 20px;
    margin-bottom: 8px;
    padding-inline: 8px;
    color: var(--gray);
    font-size: .75rem;
    font-weight: 600;
    letter-spacing: .04em;
    text-transform: uppercase;
}

.app-shell .header li.nav-section-title:first-child {
    margin-top: 0;
}

/* Unlabeled sections are set apart by a hairline instead of a caption */
.app-shell .header li.nav-section-divider {
    height: 0;
    margin-top: 12px;
    margin-bottom: 12px;
    padding: 0;
    border-top: 1px solid var(--border);
}

/* Collapsible submenu groups (e.g. an addon's submenu): the group label is a
   button reset to the same footprint as menu links, and the submenu stays
   hidden until toggled so the sidebar stays short. Structural selectors do
   the work - a group is simply "an li whose ul follows a .nav-group-label",
   so the markup needs no extra class beyond the .open state. Outside the
   media query so the mobile expanded menu behaves the same. */
.app-shell .header .nav-group-label {
    position: relative; /* anchors the menu marker (see above) */
    display: flex;
    align-items: center;
    justify-content: space-between;
    width: 100%;
    padding: 8px;
    padding-inline-start: 24px;
    background: transparent;
    border: 0;
    border-radius: var(--radius);
    color: var(--dark);
    font: inherit;
    font-weight: 500;
    text-align: start;
    cursor: pointer;
    transition: background .15s ease, color .15s ease;
}

.app-shell .header .nav-group-label:hover {
    opacity: 1;
    background: rgba(var(--black-rgb), .04);
    color: var(--black);
}

.app-shell .header .nav-group-caret {
    display: inline-block;
    color: var(--gray);
    font-size: .8em;
    transition: transform .15s ease;
}

.app-shell .header li.open > .nav-group-label .nav-group-caret {
    transform: rotate(90deg);
}

/* !important is needed because the mobile menu forces submenus open with
   display: block !important in style.css */
.app-shell .header li:not(.open) > .nav-group-label + ul {
    display: none !important;
}

/* Top-bar layout (app_layout 'topbar'): signed-in users keep the
   visitors' bar, which ends with the account menu. Mobile first: inside
   the Menu panel the account menu shows open, after a hairline, with the
   user's name on top and its links as pills; from 768px its button opens
   it as a dropdown under the bar. style.css caps the expanded menu at
   512px; the account menu grows with the admin links and an open
   dropdown group adds its links, so the cap is raised to a height no menu
   reaches, which keeps the slide. */
.header.expanded .custom-column.hidden-for-mobile {
    max-height: 2048px;
}

.header .account-menu {
    margin-top: 8px;
    padding-top: 16px;
    border-top: 1px solid var(--border);
}

.account-menu-toggle {
    display: none;
}

/* The words "Account:" stay for screen readers only */
.account-menu-label {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    border: 0;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
}

.account-avatar {
    display: inline-flex;
    flex: 0 0 32px;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    border-radius: 50%;
    background: var(--brand);
    color: var(--white);
    font-size: .9rem;
    font-weight: 600;
    line-height: 1;
}

.account-menu-who {
    display: flex;
    align-items: center;
    margin: 0 0 8px 0;
}

.account-menu-who .account-avatar {
    margin-inline-end: 12px;
}

.account-menu-who-text {
    flex: 1 1 auto;
    min-width: 0;
}

.account-menu-who-text strong {
    display: block;
    overflow-wrap: anywhere;
}

.account-menu-email {
    display: block;
    overflow: hidden;
    color: var(--gray);
    font-size: .85rem;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.account-menu-caption {
    margin: 12px 0 4px 0;
    color: var(--gray);
    font-size: .75rem;
    font-weight: 600;
    letter-spacing: .04em;
    text-transform: uppercase;
}

.account-menu-panel ul {
    margin: 0 -12px;
    padding: 0;
}

.header .account-menu-panel li {
    flex: 0 0 auto;
    margin: 0;
}

.header .account-menu-panel li a,
.header .account-menu-panel .nav-group-label {
    display: block;
    width: 100%;
    padding: 8px 12px;
    border: 0;
    border-radius: var(--radius);
    background: transparent;
    color: var(--dark);
    font: inherit;
    font-weight: 500;
    text-align: start;
    text-decoration: none;
}

.header .account-menu-panel li a:hover,
.header .account-menu-panel .nav-group-label:hover {
    background: rgba(var(--black-rgb), .05);
    background-image: none;
    color: var(--black);
    text-decoration: none;
}

.header .account-menu-panel li.active > a {
    background: rgba(var(--brand-rgb), .08);
    color: var(--brand);
    font-weight: 600;
    text-decoration: none;
}

/* A submenu inside the account menu folds like the sidebar's */
.header .account-menu-panel li > ul {
    display: block;
    position: static;
    min-width: 0;
    margin: 0;
    padding: 0;
    padding-inline-start: 12px;
    box-shadow: none;
}

.header .account-menu-panel li > ul::before {
    display: none;
}

.header .account-menu-panel li:not(.open) > .nav-group-label + ul {
    display: none !important;
}

.account-menu-panel .account-menu-end {
    margin-top: 8px;
    padding-top: 8px;
    border-top: 1px solid var(--border);
}

@media (min-width: 768px) {
    .header .account-menu {
        position: relative;
        margin-top: 0;
        padding-top: 0;
        border-top: 0;
    }

    .account-menu-toggle {
        display: flex;
        align-items: center;
        max-width: 240px;
        padding: 4px 12px 4px 4px;
        border: 1px solid var(--border);
        border-radius: 999px;
        background: transparent;
        color: var(--black);
        font-weight: 500;
    }

    .account-menu-toggle:not([disabled]):hover,
    .account-menu-toggle[aria-expanded="true"] {
        background: rgba(var(--black-rgb), .05);
        background-image: none;
    }

    /* The page shown is one of the menu's links */
    .account-menu-toggle.is-current {
        border-color: var(--brand);
    }

    .account-menu-toggle .account-avatar {
        margin-inline-end: 8px;
    }

    .account-menu-name {
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    .account-menu-caret {
        margin-inline-start: 8px;
        color: var(--gray);
        font-size: .75rem;
    }

    /* Closed, the panel is hidden from the eye, the keyboard and screen
       readers (visibility); it fades in and drops 8px into place */
    .account-menu-panel {
        visibility: hidden;
        opacity: 0;
        transform: translateY(-8px);
        transition: opacity .15s ease, transform .15s ease, visibility 0s linear .15s;
        position: absolute;
        top: calc(100% + 8px);
        inset-inline-end: 0;
        z-index: 30;
        width: 280px;
        padding: 16px 20px;
        border: 1px solid var(--border);
        border-radius: var(--radius-lg);
        background: var(--white);
        box-shadow: 0 12px 32px -8px rgba(var(--shadow-rgb), .24);
        text-align: start;
    }

    .account-menu.open .account-menu-panel {
        visibility: visible;
        opacity: 1;
        transform: none;
        transition: opacity .15s ease, transform .15s ease, visibility 0s linear 0s;
    }

    .account-menu-who {
        margin-bottom: 12px;
        padding-bottom: 12px;
        border-bottom: 1px solid var(--border);
    }
}

/* Top-bar dropdowns: a submenu of the visitors' bar (li.nav-dropdown, a
   disclosure of ui.js that carries .open on the li). Its button reads as
   one of the bar's links, with a caret. Mobile first: inside the Menu panel the submenu is
   an expandable group under its button, shown only while open (style.css
   shows every submenu there with !important, so folding it needs
   !important too). */
.header li.nav-dropdown > .nav-group-label {
    display: inline-flex;
    align-items: center;
    padding: 0;
    border: 0;
    border-radius: 0;
    background: transparent;
    color: inherit;
    font: inherit;
    cursor: pointer;
}

.header li.nav-dropdown > .nav-group-label:not([disabled]):hover,
.header li.nav-dropdown.open > .nav-group-label {
    background-image: none;
    text-decoration: underline;
    text-decoration-color: rgba(var(--black-rgb), .5);
    text-decoration-thickness: 2px;
    text-underline-offset: 8px;
}

/* The caret is drawn as a small chevron (the glyph inside it is hidden):
   pointing to the end while the group is closed, down while it is open */
.header li.nav-dropdown .nav-group-caret {
    display: inline-block;
    width: 6px;
    height: 6px;
    margin-inline-start: 8px;
    overflow: hidden;
    border-right: 2px solid var(--gray);
    border-bottom: 2px solid var(--gray);
    font-size: 0;
    transform: rotate(-45deg);
    transition: transform .15s ease;
}

.header li.nav-dropdown.open .nav-group-caret {
    transform: translateY(-2px) rotate(45deg);
}

.header li.nav-dropdown > ul {
    padding: 0;
    padding-inline-start: 16px;
}

.header li.nav-dropdown:not(.open) > ul {
    display: none !important;
}

/* From 768px the submenu is a panel under its button, styled and moving
   like the account menu's. Closed, it is hidden from the eye, the keyboard
   and screen readers (visibility), so the style.css rules that show a
   submenu on :hover and :focus-within (they only set display) no longer
   open it: the button and the pointer do, through the disclosures of ui.js. */
@media (min-width: 768px) {
    /* The chevron points down, up while the panel is open */
    .header li.nav-dropdown .nav-group-caret {
        transform: translateY(-2px) rotate(45deg);
    }

    .header li.nav-dropdown.open .nav-group-caret {
        transform: translateY(2px) rotate(225deg);
    }

    .header li.nav-dropdown > ul,
    .header li.nav-dropdown:not(.open) > ul {
        display: block !important;
    }

    .header li.nav-dropdown > ul {
        visibility: hidden;
        opacity: 0;
        transform: translateY(-8px);
        transition: opacity .15s ease, transform .15s ease, visibility 0s linear .15s;
        top: calc(100% + 8px);
        z-index: 30;
        min-width: 200px;
        padding: 8px;
        border: 1px solid var(--border);
        border-radius: var(--radius-lg);
        background: var(--white);
        box-shadow: 0 12px 32px -8px rgba(var(--shadow-rgb), .24);
        text-align: start;
    }

    .header li.nav-dropdown.open > ul {
        visibility: visible;
        opacity: 1;
        transform: none;
        transition: opacity .15s ease, transform .15s ease, visibility 0s linear 0s;
    }

    .header li.nav-dropdown > ul::before {
        display: none;
    }

    .header li.nav-dropdown > ul > li {
        padding: 0;
    }

    .header li.nav-dropdown > ul a {
        display: block;
        padding: 8px 12px;
        border-radius: var(--radius);
        color: var(--dark);
        font-weight: 500;
        text-decoration: none;
        white-space: nowrap;
    }

    .header li.nav-dropdown > ul a:hover {
        background: rgba(var(--black-rgb), .05);
        color: var(--black);
        text-decoration: none;
    }

    .header li.nav-dropdown li.active > a {
        background: rgba(var(--brand-rgb), .08);
        color: var(--brand);
        font-weight: 600;
        text-decoration: none;
    }
}

/* With reduced motion the menus open and close at once */
@media (prefers-reduced-motion: reduce) {
    .account-menu-panel,
    .account-menu.open .account-menu-panel,
    .header li.nav-dropdown > ul,
    .header li.nav-dropdown.open > ul,
    .header li.nav-dropdown .nav-group-caret,
    .header .custom-column.hidden-for-mobile,
    .header.expanded .custom-column.hidden-for-mobile {
        transition: none;
    }
}

/* The actions menu (renderActionsMenu() in system.js): a row's actions
   behind one round button with three dots, a disclosure menu of ui.js that
   puts .open on the panel. Its items are rows like the account menu's; a
   destructive one (actions-menu-danger) is set apart in the error red. On
   phones the panel opens in the flow under its button; from 768px it floats
   8px under the button at its end edge and, closed, is hidden from the eye,
   the keyboard and screen readers (visibility); open, it fades in and drops
   8px into place. */
.actions-menu-holder {
    display: inline-block;
    position: relative;
    vertical-align: middle;
    text-align: start;
}

.actions-menu-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    padding: 0;
    border: 1px solid transparent;
    border-radius: 50%;
    background: transparent;
    color: var(--brand);
}

.actions-menu-toggle:not([disabled]):hover,
.actions-menu-toggle[aria-expanded="true"] {
    border-color: var(--border);
    background: rgba(var(--black-rgb), .05);
    background-image: none;
}

.actions-menu-toggle svg {
    width: 20px;
    height: 20px;
    fill: currentColor;
}

.actions-menu {
    display: none;
    min-width: 160px;
    margin-top: 8px;
    padding: 8px;
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background: var(--white);
    box-shadow: 0 12px 32px -8px rgba(var(--shadow-rgb), .24);
    text-align: start;
    white-space: normal;
}

.actions-menu.open {
    display: block;
}

.actions-menu a,
.actions-menu button {
    display: block;
    width: 100%;
    margin: 0;
    padding: 8px 12px;
    border: 0;
    border-radius: var(--radius);
    background: transparent;
    color: var(--dark);
    font: inherit;
    font-size: .9rem;
    font-weight: 500;
    text-align: start;
    text-decoration: none;
    cursor: pointer;
}

.actions-menu a:hover,
.actions-menu button:not([disabled]):hover {
    background: rgba(var(--black-rgb), .05);
    background-image: none;
    color: var(--black);
    text-decoration: none;
}

/* A destructive item: a little space above it and the error red */
.actions-menu .actions-menu-danger {
    margin-top: 4px;
    color: var(--error-text);
}

.actions-menu .actions-menu-danger:not([disabled]):hover {
    background: rgba(var(--red-rgb), .08);
    color: var(--error-text);
}

/* From 768px the panel floats under its button. Closed, it takes no room
   at all (display: none), so a table's last rows never give their wrapper
   a scrollbar for menus nobody opened; open, it fades and drops in. */
@media (min-width: 768px) {
    .actions-menu {
        position: absolute;
        right: 0;
        z-index: 30;
        width: 224px;
    }

    .actions-menu.open {
        animation: actions-menu-in .15s ease;
    }

    /* A table scrolls inside its wrapper, which would clip the floating
       panel: while one of its rows has the menu open, the wrapper lets it
       spill over */
    .table-wrapper:has(.actions-menu.open) {
        overflow: visible;
    }
}

@keyframes actions-menu-in {
    from {
        opacity: 0;
        transform: translateY(-8px);
    }

    to {
        opacity: 1;
        transform: none;
    }
}

[dir="rtl"] .actions-menu {
    right: auto;
    left: 0;
}

/* The rest of a row's end (renderRowActions() in system.js): a link that
   opens something about the item (View, Receipt) is plain text before the
   actions, and a lone action is a small secondary button in the place of
   the actions menu, in the error red when it destroys */
.row-link {
    display: inline-block;
    margin-inline-end: 12px;
    vertical-align: middle;
}

button.row-action,
.button.row-action {
    padding: 4px 12px;
    font-size: .8rem;
    line-height: 1.4;
    vertical-align: middle;
}

button.row-action-danger,
.button.row-action-danger {
    color: var(--error-text);
}

button.row-action-danger:not([disabled]):hover,
.button.row-action-danger:not([disabled]):hover {
    background-image: linear-gradient(rgba(var(--red-rgb), .08), rgba(var(--red-rgb), .08));
    border-color: var(--error-text);
}

button.row-action-danger:focus-visible,
.button.row-action-danger:focus-visible {
    outline-color: var(--error-text);
}

/* The modal (ui.js): a native <dialog> over a dimmed page, centred, full
   width with a 16px gutter on phones and 480px wide at most. Plain by
   default, a surface like the menus' panels: it fades in as it opens (at
   once with reduced motion), the close button sits at the top end and the
   actions line up at the end. A project dresses it up through the modal
   tokens and its own .modal rules. */
.modal {
    width: calc(100% - 32px);
    max-width: var(--modal-width);
    margin: auto;
    padding: 0;
    border: 1px solid var(--border);
    border-radius: var(--modal-radius);
    background: var(--white);
    color: var(--black);
    box-shadow: var(--modal-shadow);
}

.modal[open] {
    animation: modal-fade .2s ease-out;
}

.modal::backdrop {
    /* Browsers from before 2024 hand ::backdrop no custom properties, so
       each value carries the default as its fallback */
    background: var(--modal-backdrop, rgba(0, 0, 0, .5));
    -webkit-backdrop-filter: blur(var(--modal-blur, 0px));
    backdrop-filter: blur(var(--modal-blur, 0px));
}

@keyframes modal-fade {
    from {
        opacity: 0;
    }

    to {
        opacity: 1;
    }
}

.modal-body {
    position: relative;
    padding: 24px 20px;
}

.modal-title {
    margin: 0 0 8px 0;
    padding-inline-end: 40px;
    font-size: 1.25rem;
}

/* The message of a confirm or alert box keeps its line breaks */
.modal-text {
    margin: 0 0 24px 0;
    color: var(--gray);
    white-space: pre-line;
}

.modal-close {
    position: absolute;
    top: 16px;
    right: 16px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    padding: 0;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: rgba(var(--black-rgb), .05);
    color: var(--black);
    font-size: 1.3rem;
    line-height: 1;
}

[dir="rtl"] .modal-close {
    right: auto;
    left: 16px;
}

.modal-close:not([disabled]):hover {
    background: rgba(var(--black-rgb), .1);
    background-image: none;
}

/* The cross sits on the button's own line height (style.css gives every
   span 1.5, which would push it off centre) */
.modal-close span {
    line-height: 1;
}

.modal .modal-actions {
    justify-content: flex-end;
    margin-top: 8px;
}

.modal .modal-actions > *:last-child {
    margin-inline-end: 0;
}

/* A status line under a form in a modal: muted, red for an error, gone
   while empty */
.modal-status {
    margin: 12px 0 0 0;
    color: var(--gray);
    font-size: .9rem;
}

.modal-status.is-error {
    color: var(--error-text);
}

.modal-status:empty {
    display: none;
}

/* A modal's done state: centred, under a round check */
.modal-success {
    text-align: center;
}

.modal-success-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 64px;
    height: 64px;
    margin-bottom: 16px;
    border-radius: 50%;
    background: rgba(var(--green-rgb), .12);
    color: var(--green-strong);
    font-size: 1.75rem;
}

/* The done state's title is centred, so it keeps no room for the close
   button */
.modal-success .modal-title {
    padding-inline-end: 0;
}

@media (min-width: 768px) {
    .modal-body {
        padding: 32px;
    }
}

@media (prefers-reduced-motion: reduce) {
    .actions-menu.open {
        animation: none;
    }

    .modal[open] {
        animation: none;
    }
}

/* The notices (ui.js): a stack at the top of the window, clear of the
   floating buttons at the bottom. Full width with a 16px gutter on phones;
   from 768px a 360px column at the top end, under the top bar. Each notice
   is a card with a colored edge at its start (green for success, the brand
   for info, the error color for an error) and a round close button. It
   slides in and fades out, at once with reduced motion. The stack is a
   manual popover where the browser has them, so the popover defaults are
   undone here. */
.notify-stack {
    position: fixed;
    top: 16px;
    right: 16px;
    bottom: auto;
    left: 16px;
    z-index: 50;
    width: auto;
    margin: 0;
    padding: 0;
    border: 0;
    background: transparent;
    color: var(--black);
    overflow: visible;
    pointer-events: none;
}

.notify {
    display: flex;
    align-items: flex-start;
    margin-bottom: 8px;
    padding: 12px 12px 12px 16px;
    border: 1px solid var(--border);
    border-inline-start: 4px solid var(--green);
    border-radius: var(--radius);
    background: var(--white);
    color: var(--black);
    box-shadow: 0 16px 32px -16px rgba(var(--shadow-rgb), .5);
    pointer-events: auto;
    animation: notify-enter .25s cubic-bezier(.2, .7, .2, 1);
    transition: opacity .3s ease;
}

.notify-info {
    border-inline-start-color: var(--brand);
}

.notify-error {
    border-inline-start-color: var(--error-text);
}

.notify.is-leaving {
    opacity: 0;
}

.notify-text {
    flex: 1 1 auto;
    min-width: 0;
    margin: 4px 0;
    font-size: .95rem;
    white-space: pre-line;
}

.notify-close {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    margin-inline-start: 12px;
    padding: 0;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: rgba(var(--black-rgb), .05);
    color: var(--black);
    font-size: 1.2rem;
    line-height: 1;
}

.notify-close:not([disabled]):hover {
    background: rgba(var(--black-rgb), .1);
    background-image: none;
}

@keyframes notify-enter {
    from {
        opacity: 0;
        transform: translateY(-8px);
    }

    to {
        opacity: 1;
        transform: none;
    }
}

@media (min-width: 768px) {
    .notify-stack {
        top: 88px;
        left: auto;
        width: 360px;
    }

    [dir="rtl"] .notify-stack {
        right: auto;
        left: 16px;
    }
}

/* The status line of a form (setFormStatus()): right after its buttons,
   the error color for an error, the ink for success, gone while empty */
.form-status {
    margin: 12px 0 0 0;
    color: var(--gray);
    font-size: .95rem;
}

.form-status.is-error {
    color: var(--error-text);
}

.form-status.is-success {
    color: var(--black);
}

.form-status.is-info {
    color: var(--gray);
}

.form-status:empty {
    display: none;
}

@media (prefers-reduced-motion: reduce) {
    .notify {
        animation: none;
        transition: none;
    }
}

/* Footer sits quietly on the canvas */
.footer p {
    color: var(--gray);
    font-size: .85rem;
}

/* The floating buttons sit over the bottom end corner of the screen. When
   the page shows at least one, the footer ends with an empty band as tall
   as the stack plus its offset, so at the bottom of the page every footer
   line sits above the buttons, on either side: the offset plus one button
   (--fab-offset, --fab-size), then a gap and a button more for each
   further visible button, up to four. A page without floating buttons
   gets no band. */
body:has(.fab-stack li:not([hidden])) .footer::after {
    content: "";
    display: block;
    height: calc(var(--fab-offset) + var(--fab-size));
}

/* The rules count visible buttons whatever hidden ones sit in between */
body:has(.fab-stack li:not([hidden]) ~ li:not([hidden])) .footer::after {
    height: calc(var(--fab-offset) + 2 * var(--fab-size) + var(--fab-gap));
}

body:has(.fab-stack li:not([hidden]) ~ li:not([hidden]) ~ li:not([hidden])) .footer::after {
    height: calc(var(--fab-offset) + 3 * var(--fab-size) + 2 * var(--fab-gap));
}

body:has(.fab-stack li:not([hidden]) ~ li:not([hidden]) ~ li:not([hidden]) ~ li:not([hidden])) .footer::after {
    height: calc(var(--fab-offset) + 4 * var(--fab-size) + 3 * var(--fab-gap));
}

/* Floating action buttons: a fixed stack in the bottom end corner, built
   from window.FAB_ITEMS in app.js (dark-mode toggle ships by default) */
.fab-stack {
    position: fixed;
    inset-inline-end: var(--fab-offset);
    bottom: var(--fab-offset);
    z-index: 40;
    margin: 0;
    padding: 0;
}

.fab-stack li {
    list-style: none;
}

/* Spacing between visible items only: a hidden sibling (e.g. back to top)
   renders no margin, so the stack always sits flush at its bottom offset */
.fab-stack li + li {
    margin-top: var(--fab-gap);
}

.fab-stack button {
    display: flex;
    align-items: center;
    justify-content: center;
    width: var(--fab-size);
    height: var(--fab-size);
    padding: 0;
    border-radius: 50%;
    font-size: 1.25rem;
    line-height: 1;
    box-shadow: 0 1px 3px rgba(var(--shadow-rgb), .12), 0 8px 24px rgba(var(--shadow-rgb), .16);
}
.fab-stack button svg {
    width: 20px;
    height: 20px;
}

/* The dark-mode toggle holds a moon and a sun icon and shows the mode it
   leads to: the moon while the page is light, the sun while it is dark -
   from the operating system until the user picks, then from
   body.light / body.dark */
.fab-stack .icon-sun {
    display: none;
}

@media (prefers-color-scheme: dark) {
    .fab-stack .icon-moon {
        display: none;
    }

    .fab-stack .icon-sun {
        display: block;
    }
}

body.light .fab-stack .icon-moon,
body.dark .fab-stack .icon-sun {
    display: block;
}

body.light .fab-stack .icon-sun,
body.dark .fab-stack .icon-moon {
    display: none;
}

/* -----------------------------------------------------------------------------
   6. COMPONENTS - modern surfaces for the pages rendered by app.js
   -------------------------------------------------------------------------- */

/* Page content gets breathing room below the top edge */
#app > .custom-container {
    padding-top: 32px;
    padding-bottom: 32px;
}

/* Stacked page sections: only the first keeps the full top offset, so pages
   built from several containers read as one page instead of islands with
   64px voids between them */
#app > .custom-container ~ .custom-container {
    padding-top: 0;
}

/* Inline form: a label on its own line, then a text control and its action
   button sharing one row. The pattern for mini-forms (invite by email,
   quick-add fields) so they match full forms instead of stacking raw
   full-width controls. */
.inline-form {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    margin-bottom: 16px;
    clear: both; /* never sit beside a floated legend */
}

.inline-form label {
    flex: 0 0 100%;
    margin-bottom: 8px;
}

/* Mobile: the row stacks. The control takes the full width and the
   buttons move to their own line beneath it, sharing that line with the
   gutter between them. min-width: 0 lets a control shrink below its
   intrinsic width (a select sized by its longest option, a text input by
   its default 20 characters), so it never pushes past a phone screen. */
.inline-form input:not([type="checkbox"]):not([type="radio"]),
.inline-form select {
    flex: 1 1 100%;
    width: auto;
    min-width: 0;
    margin-bottom: 0;
}

.inline-form > * + * {
    margin-top: 8px;
}

.inline-form > label + * {
    margin-top: 0;
}

/* A file input has a wide built-in minimum (its button plus the file name);
   it shrinks to its row instead of pushing a phone screen wider */
input[type="file"] {
    min-width: 0;
    max-width: 100%;
}

.inline-form button {
    flex: 1 1 auto;
    white-space: nowrap;
}

.inline-form button + button {
    margin-inline-start: 8px;
}

/* Desktop: the control takes a comfortable width and stops (a search box
   stretched across the whole screen reads as a gap between the title and
   its buttons), and the controls share one line with a margin gutter: the
   control right after the full-width label is the row start, so it gets
   none */
@media (min-width: 768px) {
    .inline-form input:not([type="checkbox"]):not([type="radio"]),
    .inline-form select {
        flex: 0 1 420px;
    }

    .inline-form > * + * {
        margin-top: 0;
        margin-inline-start: 8px;
    }

    .inline-form > label + * {
        margin-inline-start: 0;
    }

    .inline-form button {
        flex: 0 0 auto;
    }
}

/* Side-by-side fields: a .custom-row inside a form stacks its field groups
   on mobile and, from 768px, splits the width evenly between them with a
   margin gutter instead of letting the inputs shrink-wrap and sit flush
   against each other. Scoped to forms so the header/footer rows that share
   the class are untouched. */
form .custom-row {
    display: block;
}

@media (min-width: 768px) {
    form .custom-row {
        display: flex;
    }

    form .custom-row > div {
        flex: 1 1 0;
    }

    /* A button beside a field (Set Now next to a date-time field) keeps
       its label on one line, the field beside it gives up the width, and
       it stays at its own height, level with the top of the field, when
       the field wraps onto two lines */
    form .custom-row > button {
        flex: 0 0 auto;
        align-self: flex-start;
        white-space: nowrap;
    }

    form .custom-row > * + * {
        margin-inline-start: 16px;
    }
}

/* Date and time fields, drawn by getDateFieldHTML(), getTimeFieldHTML()
   and getDateTimeFieldHTML() in system.js. A date field is the browser's
   date input with the chosen date in the user's format beside it (below it
   on mobile); a time field is a row of hour, minute and AM/PM selects; a
   date-time field puts the two together. The field carries the 16px bottom
   margin its controls would have, and its controls wrap at narrow widths. */
.date-field,
.time-field,
.datetime-field {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    margin-bottom: 16px;
}

.date-field input.date-field-input {
    flex: 1 1 100%;
    width: auto;
    min-width: 0;
    margin-bottom: 0;
}

.date-field-label {
    flex: 0 0 100%;
    margin-top: 4px;
    color: var(--gray);
    font-size: .85rem;
}

/* The time selects hold two digits or AM/PM, so they take a narrower
   inline padding than other controls and the three fit side by side on a
   320px screen */
.time-field select {
    flex: 0 0 auto;
    width: auto;
    margin-bottom: 0;
    padding-left: 12px;
    padding-right: 8px;
}

.time-field-separator {
    padding: 0 4px;
}

.time-field .time-field-period {
    margin-inline-start: 8px;
}

.datetime-field > .date-field {
    flex: 1 1 100%;
    margin-bottom: 0;
}

.datetime-field > .time-field {
    margin-top: 8px;
    margin-bottom: 0;
}

@media (min-width: 768px) {
    .date-field input.date-field-input {
        flex: 0 0 auto;
        width: 240px;
    }

    .date-field-label {
        flex: 0 1 auto;
        margin-top: 0;
        margin-inline-start: 12px;
    }

    /* The date and the time sit side by side with a 16px gutter and 8px
       between lines. Each carries the gutter as its start and top margin
       and the field takes it back with the same negative margins, so when
       a narrow column wraps the time under the date, both stay flush with
       the label instead of the time starting 16px in. */
    .datetime-field {
        margin-top: -8px;
        margin-inline-start: -16px;
    }

    .datetime-field > .date-field {
        flex: 0 1 auto;
        margin-top: 8px;
        margin-inline-start: 16px;
    }

    .datetime-field > .time-field {
        margin-top: 8px;
        margin-inline-start: 16px;
    }
}

/* In an .inline-form row (the list filters) a date field is one control of
   the row: full width on mobile, from 768px a compact input with its label
   beside it, next to the row's other controls */
.inline-form .date-field {
    flex: 1 1 100%;
    margin-bottom: 0;
}

.inline-form .date-field input.date-field-input {
    flex: 1 1 100%;
}

@media (min-width: 768px) {
    .inline-form .date-field {
        flex: 0 1 auto;
    }

    .inline-form .date-field input.date-field-input {
        flex: 0 0 auto;
        width: 200px;
    }
}

/* Create/edit forms in the app shell render as cards automatically, so
   every addon's form matches the profile page without extra markup.
   Only forms sitting directly in a page section qualify: .inline-form
   rows (search bars, quick-adds) stay flat, and forms that build their
   own fieldset cards inside are left alone. */
.app-shell #app > .custom-container > form:not(.inline-form):not(:has(fieldset)) {
    padding: 24px;
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background: var(--white);
    box-shadow: 0 1px 1px rgba(var(--shadow-rgb), .03), 0 12px 32px -20px rgba(var(--shadow-rgb), .16);
}

.app-shell #app > .custom-container > form > :last-child {
    margin-bottom: 0;
}

/* Page titles: dashboard-sized instead of the marketing-scale style.css h1,
   with tighter tracking at that size */
.page-header h1,
.custom-container > h1 {
    font-size: 1.75rem;
    line-height: 1.25;
    letter-spacing: -0.02em;
    margin-bottom: 16px;
}

/* Inside .page-header the hairline below carries the spacing */
.page-header h1 {
    margin-bottom: 0;
}

/* A heading above a table or list is a caption for it, not a second title */
.custom-container > h3 {
    margin-bottom: 12px;
    font-size: 1rem;
    font-weight: 600;
    color: var(--dark);
}

/* Inline validation: the message sits tight under its control (inputs
   carry a 16px bottom margin, so the negative top margin closes the gap)
   and the control itself gets a red hairline, both in --error-text so
   they read in either mode. Errors render via showFieldError()/
   validateForm() in system.js - never native browser bubbles (forms set
   novalidate). */
.field-error {
    margin: -12px 0 16px;
    color: var(--error-text);
    font-size: .85rem;
}

/* The input selector repeats the :not() list of the input rule below, so
   the red edge outweighs its hairline border */
input.input-invalid:not([type="checkbox"]):not([type="radio"]):not([type="file"]),
select.input-invalid,
textarea.input-invalid,
.input-invalid input.date-field-input,
.input-invalid .time-field select,
.time-field.input-invalid select {
    border-color: var(--error-text);
}

/* Inside .inline-form rows the (dynamically inserted) error takes its
   own full line below the row instead of squeezing between controls.
   showFieldError() inserts it right after the input, i.e. before the
   button in source order; order:1 pushes it past the button so the
   input+button stay together on one row and the error drops beneath. */
.inline-form .field-error {
    flex: 0 0 100%;
    order: 1;
    margin: 8px 0 0;
}

/* Field hints: <small> notes inside labels and hint paragraphs read as
   quiet muted text. The convention is <small> with no literal
   parentheses - "Leave blank to keep your current password", not
   "(Leave blank...)". */
label small,
legend small,
form p small,
p small {
    color: var(--gray);
    font-weight: normal;
}

/* Text that follows a form or button (helper links under the login form)
   gets clear space */
form + p,
button + p,
.button + p {
    margin-top: 16px;
}

/* No trailing space at the bottom of cards */
fieldset > :last-child,
.wrapper > :last-child {
    margin-bottom: 0;
}

/* Inputs: hairline border, rounded, ink focus ring */
input:not([type="checkbox"]):not([type="radio"]):not([type="file"]),
select,
textarea {
    border: 1px solid rgba(var(--black-rgb), .14);
    border-radius: var(--radius);
    background: var(--field);
    transition: border-color .15s ease, box-shadow .15s ease;
}

/* Placeholders take the muted ink instead of the browser's fixed gray,
   which falls under 3:1 on the dark field */
input::placeholder,
textarea::placeholder {
    color: var(--gray);
    opacity: 1;
}

input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):focus,
select:focus,
textarea:focus {
    outline: none;
    border-color: var(--brand);
    box-shadow: 0 0 0 3px rgba(var(--brand-rgb), .12);
}

/* Form sections render as cards; the legend sits inside the card
   (floated full-width) instead of straddling the fieldset border. Cards
   carry their edge with the hairline; the shadow is only a hint of depth.
   Browsers give a fieldset min-width: min-content, so a select holding a
   long option (a name with its email address) would stretch the card past
   a phone screen; min-width: 0 keeps the card at its column's width and the
   control shrinks inside it. */
fieldset {
    min-width: 0;
    margin-bottom: 16px;
    padding: 24px;
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background: var(--white);
    box-shadow: 0 1px 1px rgba(var(--shadow-rgb), .03), 0 12px 32px -20px rgba(var(--shadow-rgb), .16);
}

/* Legends speak in the same voice as table headers: a micro-label */
legend {
    float: inline-start;
    width: 100%;
    padding: 0;
    margin-bottom: 16px;
    font-size: .75rem;
    font-weight: 600;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--gray);
}

/* Option rows inside a card keep an even rhythm; the card's own last-child
   rule takes the trailing margin off */
fieldset > div {
    margin-bottom: 8px;
}

input[type="radio"] + label {
    display: inline;
    margin-inline-start: 8px;
    font-weight: 500;
}

/* The floated legend must not squeeze what follows it: flex/grid/overflow
   boxes (unlike normal block flow) get placed in the leftover space beside
   a float, which for a 100%-wide float is a zero-width sliver at the edge.
   Clearing the first sibling puts everything back below the legend. */
legend + * {
    clear: both;
}

/* Tables render as cards: rounded white surface, micro-label headers,
   row hairlines, no zebra */
.table-wrapper {
    overflow: auto;
    margin-bottom: 24px;
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background: var(--white);
    box-shadow: 0 1px 1px rgba(var(--shadow-rgb), .03), 0 12px 32px -20px rgba(var(--shadow-rgb), .16);
}

/* Separate borders with zero spacing draw exactly like collapsed ones here
   (cells carry only a bottom hairline), and they let a pinned cell take its
   hairline along when the table scrolls beneath it */
table {
    border-collapse: separate;
    border-spacing: 0;
}

th,
td {
    border: none;
    border-bottom: 1px solid var(--border);
    padding: 12px 16px;
}

th {
    background: var(--light);
    color: var(--gray);
    font-size: .75rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .05em;
    text-align: start;
}

td {
    font-size: .9rem;
    font-variant-numeric: tabular-nums; /* digits line up in data columns */
}

tr:nth-child(even) td {
    background: transparent;
}

tr:last-child td {
    border-bottom: none;
}

/* The first column stays put while a wide table scrolls, so a row keeps its
   label in view however many columns it carries. A table whose first column
   must never wrap (short labels beside many numeric columns) adds
   white-space: nowrap to it in the project theme. Written as tbody tr td so
   it outranks the even-row reset above (tr:nth-child(even) td), which would
   otherwise leave the pinned cell transparent on every second row. */
thead tr th:first-child,
tbody tr td:first-child {
    position: sticky;
    left: 0;
    z-index: 1;
    background: var(--white);
}

thead tr th:first-child {
    background: var(--light);
}

/* The column's edge exists only while other columns slide under it: system.js
   sets is-scrolled on the wrapper as it scrolls sideways, and the hairline
   plus shadow show that as layering, not as a cut. A table that fits its
   wrapper never shows an edge. */
.table-wrapper.is-scrolled thead tr th:first-child,
.table-wrapper.is-scrolled tbody tr td:first-child {
    box-shadow: 1px 0 0 var(--border), 12px 0 16px -12px rgba(var(--shadow-rgb), .10);
}

/* The row under the pointer lifts as one, pinned cell included */
tbody tr:hover td,
tbody tr:hover td:first-child {
    background: var(--light);
}

/* Code blocks match the card language */
pre {
    border-radius: var(--radius);
}

hr {
    border-top-color: var(--border);
}

/* -----------------------------------------------------------------------------
   7. DETAIL SURFACES - read-only pages join the same card language as forms,
   fieldsets and tables so every page reads as one system
   -------------------------------------------------------------------------- */

/* Login, register and password pages: one narrow centred column, so the
   form reads as a card-sized task instead of stretching across the screen */
.login-box {
    max-width: 480px;
    margin: 0 auto;
}

/* Generic surface for read-only content that isn't a form, fieldset, table or
   definition list (detail sub-sections, the dashboard welcome, empty states).
   Same five-property recipe as the form/fieldset/table cards. Named .panel
   so it never meets the flatter, shadowless .card some addons draw. */
.panel {
    margin-bottom: 24px;
    padding: 24px;
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background: var(--white);
    box-shadow: 0 1px 2px rgba(var(--shadow-rgb), .04);
}

.panel > :last-child {
    margin-bottom: 0;
}

/* Definition lists are the label/value layout for every detail page. <dl> is
   used only by detail views, so styling the bare element cards the generic
   renderer with no markup change; hand-written pages add .detail-list for
   clarity. Card matches the form/table surface; on desktop the rows align on a
   label column with hairline separators, echoing the table treatment. */
dl,
.detail-list {
    margin: 0 0 24px;
    padding: 8px 24px;
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background: var(--white);
    box-shadow: 0 1px 2px rgba(var(--shadow-rgb), .04);
}

dl dt,
.detail-list dt {
    color: var(--gray);
    font-size: .75rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .05em;
}

dl dd,
.detail-list dd {
    margin: 0;
    margin-top: 4px;
    color: var(--black);
    font-size: .95rem;
    overflow-wrap: anywhere;
}

/* Mobile: the label stacks above its value with a tighter rhythm */
dl dt,
.detail-list dt {
    margin-top: 16px;
}

dl dt:first-of-type,
.detail-list dt:first-of-type {
    margin-top: 0;
}

/* Desktop: a wrapping flex row where each label takes 25% and its value
   the other 75%, so the pair fills a line and every dt/dd pair (source
   order) sits on a row of its own */
@media (min-width: 768px) {
    dl,
    .detail-list {
        display: flex;
        flex-wrap: wrap;
        padding: 0 24px;
    }

    dl dt,
    .detail-list dt {
        flex: 0 0 25%;
        width: 25%;
    }

    dl dd,
    .detail-list dd {
        flex: 0 0 75%;
        width: 75%;
        min-width: 0;
    }

    /* Label and value share one line height, so the small label sits
       centred on the first line of its value, a wrapped value included */
    dl dt,
    .detail-list dt,
    dl dd,
    .detail-list dd {
        margin-top: 0;
        padding: 12px 0;
        border-top: 1px solid var(--border);
        line-height: 24px;
    }

    /* First row (first label + its value) drops the top rule */
    dl dt:first-of-type,
    .detail-list dt:first-of-type,
    dl dt:first-of-type + dd,
    .detail-list dt:first-of-type + dd {
        border-top: none;
    }

    dl dd,
    .detail-list dd {
        padding-inline-start: 16px;
    }
}

/* Grid - no gap. .card-grid (the row) > .custom-column > .inner-column >
   content. The column only takes its exact share of the row (100% on
   mobile, 50% from 768px, 33.333% from 1024px) and carries no padding; the
   .inner-column pads by 8px, half of the 16px gutter on every side, and
   the row's -8px margin takes the outer half back so the content sits
   flush with the container. Inside goes a .wrapper or .card surface, or
   plain content. Column and inner column are flex boxes and the surface
   takes flex: 1 1 auto, so every card in a row has the same height. A page
   wanting other shares changes only the flex-basis of its columns. */
.card-grid {
    display: flex;
    flex-wrap: wrap;
    margin: -8px;
}

.card-grid > .custom-column {
    display: flex;
    flex: 0 0 100%;
    width: 100%;
    min-width: 0;
}

.card-grid > .custom-column > .inner-column {
    display: flex;
    flex: 1 1 auto;
    min-width: 0;
    padding: 8px;
}

.card-grid > .custom-column > .inner-column > .wrapper,
.card-grid > .custom-column > .inner-column > .card {
    flex: 1 1 auto;
    min-width: 0;
    padding: 24px;
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background: var(--white);
    box-shadow: 0 1px 2px rgba(var(--shadow-rgb), .04);
}

@media (min-width: 768px) {
    .card-grid > .custom-column {
        flex-basis: 50%;
        width: 50%;
    }
}

@media (min-width: 1024px) {
    .card-grid > .custom-column {
        flex-basis: 33.333%;
        width: 33.333%;
    }
}

/* Button rows: used across nearly every form and detail page. A wrapping
   flex row where each action spaces itself with sibling margins, keeping
   actions 8px apart in both directions with clear space above the row
   (16px container + 8px child top margin = 24px before the first row). */
.form-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    margin-top: 16px;
}

.form-actions > * {
    margin-inline-end: 8px;
    margin-top: 8px;
}

/* Upload feedback: a native progress bar in the brand colour with a status
   line beneath it, shown while a form uploads its file and then saves.
   Forms toggle .hidden on the box and drop the bar's value attribute for
   the indeterminate state (the server is saving, nothing left to measure). */
.upload-status {
    margin-top: 16px;
}

.upload-status progress {
    display: block;
    width: 100%;
    height: 8px;
    margin-bottom: 8px;
    border: 0;
    border-radius: var(--radius);
    background: rgba(var(--black-rgb), .08);
    accent-color: var(--brand);
    overflow: hidden;
}

.upload-status progress::-webkit-progress-bar {
    background: rgba(var(--black-rgb), .08);
    border-radius: var(--radius);
}

.upload-status progress::-webkit-progress-value {
    background: var(--brand);
    border-radius: var(--radius);
}

.upload-status progress::-moz-progress-bar {
    background: var(--brand);
    border-radius: var(--radius);
}

.upload-status p {
    margin: 0;
    color: var(--gray);
    font-size: .85rem;
}

/* Form-level error (a failed save, as opposed to one invalid field): a
   line above the actions in the same voice as .field-error */
.form-error {
    margin: 16px 0 0;
    color: var(--error-text);
    font-size: .85rem;
}