/* Color tokens. Two-tier: Radix gray primitives + semantic aliases.
   Dark mode (future) swaps the --gray-* values; semantic tokens follow. */
:root {
    /* Tell the UA this site supports both modes; affects scrollbars and
       default form-control styling so they don't look out of place. */
    color-scheme: light dark;

    /* Radix gray (light) */
    --gray-1:  #fcfcfc;
    --gray-2:  #f9f9f9;
    --gray-3:  #f0f0f0;
    --gray-4:  #e8e8e8;
    --gray-5:  #e0e0e0;
    --gray-6:  #d9d9d9;
    --gray-7:  #cecece;
    --gray-8:  #bbbbbb;
    --gray-9:  #8d8d8d;
    --gray-10: #838383;
    --gray-11: #646464;
    --gray-12: #202020;

    /* Surfaces */
    --color-bg-primary:    #fff;
    --color-bg-secondary:  var(--gray-2);  /* table header, calendar tooltip th */
    --color-bg-hover:      var(--gray-3);  /* button / menu hover */
    --color-bg-stripe:     var(--gray-2);  /* table row alternation */
    --color-bg-empty-cell: var(--gray-3);  /* empty calendar cell */

    /* Text */
    --color-text-primary:   var(--gray-12);
    --color-text-secondary: var(--gray-11); /* labels, captions, bar chart labels */
    --color-text-tertiary:  var(--gray-10); /* empty-state, separator */
    --color-text-chart:     var(--gray-9);  /* D3 chart annotations */
    --color-text-light:     #fff;            /* text on dark/colored bg (absolute) */
    --color-text-dark:      #000;            /* text on bright/colored bg (absolute) */
    --color-text-emphasis:  #000;            /* one shade past primary; flips with mode */
    --color-text-error:     #E63946;
    /* Success, as text and as a filled mark. Split from --color-accent-selected
       because that value is ~3.6:1 on the dark surface and misses the 4.5:1
       floor for body text; dark steps it up to --color-accent (~6.9:1). The
       -contrast token is what goes ON the filled mark, -pending the ring of
       one not yet confirmed. */
    --color-text-success:     var(--color-accent-selected);
    --color-success-contrast: #fff;
    --color-success-pending:  #6ba06f;

    /* Borders */
    --color-border-subtle:         var(--gray-3); /* faint dividers */
    --color-border-standard:       var(--gray-5); /* most borders */
    --color-border-strong:         var(--gray-7); /* section dividers, button group outlines */
    --color-border-month-divider:  var(--color-bg-primary); /* calendar month separator */

    /* Accent / actions */
    --color-accent:            #4CAF50;
    --color-accent-hover:      #45a049;
    --color-accent-selected:   #2e7d32;
    --color-accent-focus-ring: rgba(76, 175, 80, 0.2);
    --color-error-focus-ring:  rgba(230, 57, 70, 0.2);  /* matches --color-text-error */
    --color-destructive:       #c0392b;
    --color-highlight:         #ffcc00; /* play-square hover */

    /* Part colors (instruments). va2 (second viola, quintets) is a medium
       blue — it stays in the blue/green family because it only ever
       appears next to v1/v2/va (play squares show the user's own part;
       the dashboard and network views fold VA2 into VA, so it never sits
       beside vc). Theme-invariant like the others: validated OKLab ΔE vs
       those three (normal 21.2, protan/deutan 19.0, tritan 12.7) and
       ≥3:1 contrast on both surfaces (3.3 on white, 5.8 on #111). */
    --color-part-v1:        #1ceaf9;
    --color-part-v2:        #01c472;
    --color-part-va:        #007961;
    --color-part-va2:       #5b8ee0;
    --color-part-vc:        #003b85;
    --color-part-fallback:  var(--gray-9);
    --color-part-v1-text:   var(--color-text-dark);  /* dark text on bright cyan */
    --color-part-v2-text:   var(--color-text-light);
    --color-part-va-text:   var(--color-text-light);
    --color-part-vc-text:   var(--color-text-light);

    /* Shadows */
    --color-shadow-subtle: rgba(0, 0, 0, 0.12);
    --color-shadow-light:  rgba(0, 0, 0, 0.1);
}

/* Dark mode. Swap the Radix gray scale to grayDark; semantic tokens flow
   through automatically (e.g. --color-text-primary still resolves to
   var(--gray-12), which now points at near-white #eee instead of #202020).
   Only tokens whose mapping doesn't naturally invert get an explicit override.

   Applied via two parallel paths:
   - `@media (prefers-color-scheme: dark) :root:not([data-theme])` — system
     dark mode, when the user hasn't manually overridden.
   - `:root[data-theme="dark"]` — explicit user choice (light is the base
     :root, so it needs no override block).

   The blocks duplicate; CSS has no syntactic way to share a block between an
   @media-scoped selector and an attribute-scoped selector. Keep them in sync.

   The theme manager (src/themeManager.js) sets/removes data-theme on <html>;
   components that bake colors at render (calendarComponent) re-render via
   subscribe(); see CLAUDE.md for the contract. */
@media (prefers-color-scheme: dark) {
    :root:not([data-theme]) {
        /* Radix grayDark */
        --gray-1:  #111111;
        --gray-2:  #191919;
        --gray-3:  #222222;
        --gray-4:  #2a2a2a;
        --gray-5:  #313131;
        --gray-6:  #3a3a3a;
        --gray-7:  #484848;
        --gray-8:  #606060;
        --gray-9:  #6e6e6e;
        --gray-10: #7b7b7b;
        --gray-11: #b4b4b4;
        --gray-12: #eeeeee;

        color-scheme: dark;

        --color-bg-primary:    var(--gray-1);
        --color-text-emphasis: #fff;
        --color-shadow-subtle: rgba(0, 0, 0, 0.5);
        --color-shadow-light:  rgba(0, 0, 0, 0.4);

        --color-text-success:     var(--color-accent);
        --color-success-contrast: var(--gray-1);
        --color-success-pending:  #3d6f42;
    }

    :root:not([data-theme]) #tabs button {
        background-color: transparent;
    }
}

:root[data-theme="dark"] {
    --gray-1:  #111111;
    --gray-2:  #191919;
    --gray-3:  #222222;
    --gray-4:  #2a2a2a;
    --gray-5:  #313131;
    --gray-6:  #3a3a3a;
    --gray-7:  #484848;
    --gray-8:  #606060;
    --gray-9:  #6e6e6e;
    --gray-10: #7b7b7b;
    --gray-11: #b4b4b4;
    --gray-12: #eeeeee;

    color-scheme: dark;

    --color-bg-primary:    var(--gray-1);
    --color-text-emphasis: #fff;
    --color-shadow-subtle: rgba(0, 0, 0, 0.5);
    --color-shadow-light:  rgba(0, 0, 0, 0.4);

    --color-text-success:     var(--color-accent);
    --color-success-contrast: var(--gray-1);
    --color-success-pending:  #3d6f42;
}

:root[data-theme="dark"] #tabs button {
    background-color: transparent;
}

body, html {
    margin: 0;
    padding: 0;
    height: 100%;
    width: 100%;
    font-size: 16px; /* Adjust base font-size if needed */
    color: var(--color-text-primary);
    background-color: var(--color-bg-primary);
}

/* iOS standalone safe-area handling. viewport-fit=cover (set in <head>) makes
   env(safe-area-inset-*) report real values on notched/Dynamic Island devices,
   so we have to lay out around the status bar, home indicator, and (in
   landscape) the side notches. body padding handles bottom + sides for normal
   in-flow content; absolute/fixed chrome (#menu, fullscreen overlay) handles
   its own top inset since absolute positioning ignores body padding. */
body {
    padding-bottom: env(safe-area-inset-bottom);
    padding-left: env(safe-area-inset-left);
    padding-right: env(safe-area-inset-right);
}
h1 {
    margin-top: 5px;

}

/* Container for the hamburger menu */
#menu {
    position: absolute;
    /* Keep the menu clear of the Dynamic Island / status bar (top inset) and
       the landscape side notch (left inset) when running standalone with
       viewport-fit=cover. Both fall back to 0 in normal mobile Safari. */
    top: env(safe-area-inset-top);
    left: env(safe-area-inset-left);
    padding: 10px;
    z-index: 1000;
}

/* Big tap target around the icon (~44×44) without enlarging the icon.
   Filled rounded backdrop scopes the icon as an obvious button — important on
   mobile where the icon sits in the same left column as the "All" date-range
   button and could otherwise be mistaken for inline UI. */
.hamburger-menu {
    cursor: pointer;
    padding: 8px;
    margin: -8px -8px 2px -8px;
    -webkit-tap-highlight-color: transparent;
    background-color: var(--color-bg-hover);
    border-radius: 8px;
    transition: background-color 0.15s;
}

.hamburger-menu:active {
    background-color: var(--gray-5);
}

@media (hover: hover) {
    .hamburger-menu:hover {
        background-color: var(--gray-5);
    }
}

.bar {
    display: block;
    width: 28px;
    height: 3px;
    margin: 5px 0;
    background-color: var(--color-text-primary);
    border-radius: 1px;
}

.menu-items {
    display: none;
    background-color: var(--color-bg-primary);
    padding: 4px 0;
    border: 1px solid var(--color-border-standard);
    border-radius: 8px;
    box-shadow: 0 4px 12px var(--color-shadow-subtle);
    min-width: 220px;
}

.menu-item {
    display: flex;
    align-items: center;
    gap: 10px;
    color: var(--color-text-primary);
    text-decoration: none;
    padding: 12px 16px;
    margin: 0;
    font-size: 15px;
    border-top: 1px solid var(--color-border-subtle);
    -webkit-tap-highlight-color: transparent;
}

.menu-item:first-child {
    border-top: none;
}

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

/* Inline SVG icons inherit the menu-item's text color. Disable pointer
   events so clicks always register on the <a>, never on the <svg>. */
.menu-icon {
    flex-shrink: 0;
    pointer-events: none;
}

/* Stronger divider above the first action item, separating it visually from
   the navigation items above. Only matches the first action-classed item
   because subsequent action items follow another action item. */
.menu-item:not(.menu-item--action) + .menu-item.menu-item--action {
    border-top: 1px solid var(--color-border-strong);
    margin-top: 4px;
}

.menu-item--destructive {
    color: var(--color-destructive);
}

/* Version row highlights in the accent color only when a newer deploy is
   available; otherwise it reads as a plain "Up to date" status line. Toggled by
   App._checkVersion(). */
.menu-item--update {
    color: var(--color-accent);
    font-weight: 600;
}

@media (hover: hover) {
    .menu-item:hover {
        background-color: var(--color-bg-hover);
    }
}

@media only screen and (max-width: 600px) {
    .menu-items {
        min-width: 240px;
    }
    .menu-item {
        padding: 14px 18px;
        font-size: 16px;
    }

    /* The hamburger is position: absolute, so layout treats it as
       zero-height. On mobile its visible backdrop extends to ~y=54 and
       overlaps the filter row directly below the page h1. Push the bottom
       margin of the h1 out so whatever follows it (date filter, calendar
       grid, dashboard filter) clears the hamburger zone via margin
       collapse — while letting the centered h1 itself stay in the header
       band, sharing the page-top zone with the corner hamburger. */
    h1 {
        margin-bottom: 30px;
    }
}

#dateSlider, #tabs {
    display: flex;
    justify-content: space-around;
    align-items: center;
    margin: 5px 0;
}

/* Row 2 of the header: Part filter (left) + Player dropdown pushed right
   for visual separation. */
#radioButtons {
    display: flex;
    justify-content: flex-start;
    align-items: center;
    flex-wrap: wrap;
    gap: 10px;
    margin: 5px 0;
    padding: 0 5px;
    box-sizing: border-box;
}

.player-filter {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    /* Take remaining horizontal space on the row, pushing the dropdown to
       the right edge. If the row wraps on a very narrow viewport, the
       Player widget lands on its own line at the right. */
    margin-left: auto;
}

.player-filter-label {
    color: var(--color-text-secondary);
    font-size: 13px;
}

/* Date range filter (segmented button group + custom date picker) */
.date-filter-container {
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    justify-content: flex-start;
    gap: 10px;
    width: 100%;
    overflow-x: auto;
    padding: 0 5px;
    box-sizing: border-box;
    -webkit-overflow-scrolling: touch;
}

.date-range-buttons {
    display: inline-flex;
    flex-shrink: 0;
    border: 1px solid var(--color-border-strong);
    border-radius: 6px;
    overflow: hidden;
    background: var(--color-bg-primary);
}

.date-range-btn {
    padding: 6px 14px;
    background: var(--color-bg-primary);
    border: none;
    border-right: 1px solid var(--color-border-strong);
    cursor: pointer;
    font: inherit;
    color: var(--color-text-primary);
    transition: background-color 0.15s, color 0.15s;
    -webkit-tap-highlight-color: transparent;
    white-space: nowrap;
}

.date-range-btn:last-child {
    border-right: none;
}

.date-range-btn:hover {
    background: var(--color-bg-hover);
}

.date-range-btn.active {
    background: var(--color-accent);
    color: var(--color-text-light);
}

.custom-date-range {
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    gap: 6px;
    flex-shrink: 0;
}

.custom-date-sep {
    color: var(--color-text-tertiary);
    font-size: 14px;
}

.custom-date-input {
    padding: 4px 6px;
    /* 16px prevents iOS from zooming on focus */
    font-size: 16px;
    border: 1px solid var(--color-border-strong);
    border-radius: 4px;
    background: var(--color-bg-primary);
    color: var(--color-text-primary);
    -webkit-appearance: none;
    appearance: none;
    box-sizing: border-box;
    /* Just enough room for YYYY-MM-DD on all platforms */
    min-width: 130px;
}

/* Part filter (V1 / V2 / VA / ANY) — same segmented-button look as the date
   range. Active state uses each part's color so the filter doubles as a
   colour legend for the part palette used throughout the app. */
.part-buttons {
    display: inline-flex;
    flex-shrink: 0;
    border: 1px solid var(--color-border-strong);
    border-radius: 6px;
    overflow: hidden;
    background: var(--color-bg-primary);
}

.part-btn {
    padding: 6px 14px;
    background: var(--color-bg-primary);
    border: none;
    border-right: 1px solid var(--color-border-strong);
    cursor: pointer;
    font: inherit;
    color: var(--color-text-primary);
    transition: background-color 0.15s, color 0.15s;
    -webkit-tap-highlight-color: transparent;
    white-space: nowrap;
}

.part-btn:last-child {
    border-right: none;
}

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

.part-btn.active {
    color: var(--color-text-light);
}

/* Active backgrounds — part colors. JS reads these same tokens via
   getComputedStyle so the SPA + CSS stay in sync. */
.part-btn.active[data-part="V1"] {
    background: var(--color-part-v1);
    color: var(--color-part-v1-text);
}

.part-btn.active[data-part="V2"] {
    background: var(--color-part-v2);
    color: var(--color-part-v2-text);
}

.part-btn.active[data-part="VA"] {
    background: var(--color-part-va);
    color: var(--color-part-va-text);
}

/* The log form writes the sheet's own vocabulary, which distinguishes the two
   viola seats; processRow folds VA1 back to VA on the way in, so the Home
   filter above never needs these two. */
.part-btn.active[data-part="VA1"] {
    background: var(--color-part-va);
    color: var(--color-part-va-text);
}

.part-btn.active[data-part="VA2"] {
    background: var(--color-part-va2);
    color: var(--color-text-light);
}

.part-btn.active[data-part="ANY"] {
    background: var(--color-accent);
}

@media only screen and (max-width: 800px) {
    .date-filter-container {
        gap: 6px;
    }

    .date-range-btn,
    .part-btn {
        padding: 7px 10px;
    }

    .custom-date-input {
        padding: 3px 4px;
        min-width: 120px;
    }
}

#tabs {
    overflow-x: auto;
    white-space: nowrap;
    -webkit-overflow-scrolling: touch; /* Smooth scrolling on touch devices */
    justify-content: flex-start; /* Aligns tabs to the start */
    max-width: 100%; /* Adjust as necessary */
}

#tabs button {
    display: inline-block; /* Inline-block for horizontal layout */
    background-color: var(--color-bg-hover);
    color: var(--color-text-primary); /* explicit — browser default doesn't inherit body color */
    border: 1px solid var(--color-border-strong);
    border-bottom: none;
    padding: 5px 10px;
    margin-right: 2px;
    cursor: pointer;
    transition: background-color 0.3s;
    flex-shrink: 0;
}

#tabs button:hover {
    background-color: var(--gray-5);
}

#tabs button.active-tab-button {
    background-color: var(--color-bg-primary);
    border-top: 2px solid var(--color-accent); /* Highlight color for active tab */
}

.tab {
    border: 1px solid var(--color-border-strong);
    padding: 10px;
}

/* Per-composer data table (rendered by tableComponent.js). */
.data-table th {
    border: 1px solid var(--color-border-standard);
    background-color: var(--color-bg-secondary);
}

.data-table tr {
    border: 1px solid var(--color-border-standard);
}

.data-table tbody tr:nth-child(even) {
    background-color: var(--color-bg-stripe);
}

.data-table td {
    border: 1px solid var(--color-border-standard);
}

.tab, .tooltip {
    display: none;
}

.active-tab {
    display: block;
}

.random-button-container {
    margin: 10px 0;
    display: flex;
    align-items: center;
}

.random-button {
    padding: 5px 10px;
    cursor: pointer;
}

.random-work-display {
    margin-left: 10px;
    color: var(--color-text-tertiary);
}

.tooltip {
    position: absolute;
    background: var(--color-bg-primary);
    border: 1px solid var(--color-text-emphasis);
    padding: 5px;
    border-radius: 5px;
    /* Above the network's fullscreen overlay (z-index 1000) so tooltips
       stay visible when interacting with the lightbox. */
    z-index: 1001;
    /* Never wider or taller than the viewport (matters on phones — the
       calendar day tooltip holds a full session table). positionTooltip()
       clamps placement; these clamp the box itself, scrolling if needed. */
    max-width: min(480px, calc(100vw - 20px));
    max-height: min(70vh, 520px);
    overflow-y: auto;
    box-sizing: border-box;
}

.tooltip h4 {
    margin: 0;
}

.tooltip p {
    margin: 5px 0 0 0;
}

.calendar-tooltip-table {
    margin-top: 10px;
    border-collapse: collapse;
    font-size: 12px;
    width: 100%;
}

.calendar-tooltip-table th {
    background: var(--color-bg-secondary);
    padding: 4px 6px;
    text-align: left;
    border-bottom: 1px solid var(--color-border-strong);
    font-weight: bold;
}

.calendar-tooltip-table td {
    padding: 4px 6px;
    border-bottom: 1px solid var(--color-border-subtle);
}

.calendar-tooltip-table tbody tr:last-child td {
    border-bottom: none;
}

.tooltip-close {
    /* Sticky (not absolute) so it stays reachable when a long tooltip
       scrolls (max-height + overflow-y above). float keeps it top-right. */
    position: sticky;
    float: right;
    top: 2px;
    margin-left: 8px;
    font-size: 20px;
    font-weight: bold;
    color: var(--color-text-chart);
    cursor: pointer;
    line-height: 1;
    user-select: none;
}

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

.calendar-top {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: 20px;
    max-width: 1000px;
    margin: 0 auto 6px;
}

/* Calendar fullscreen (vertical) mode — same lightbox pattern as the
   musician network. The button element carries .network-fullscreen-btn for
   its look; .calendar-fullscreen-btn only positions it. */
#calendar {
    position: relative;
}

.calendar-fullscreen-btn {
    position: absolute;
    top: 6px;
    right: 6px;
    margin-left: 0;
    z-index: 10;
}

#calendar.fullscreen {
    position: fixed;
    top: 0;
    left: 0;
    width: 100vw;
    height: 100vh;
    /* dvh tracks the actual visible viewport under mobile browser chrome;
       older engines keep the 100vh fallback. */
    height: 100dvh;
    z-index: 1000;
    background: var(--color-bg-primary);
    /* Slim padding to maximize grid height; expanded on standalone iOS to
       clear the status bar / home indicator / notches. */
    padding-top: max(10px, env(safe-area-inset-top));
    padding-right: max(10px, env(safe-area-inset-right));
    padding-bottom: max(10px, env(safe-area-inset-bottom));
    padding-left: max(10px, env(safe-area-inset-left));
    /* The year columns pan horizontally; vertical scroll is a fallback for
       very short viewports where the min cell size doesn't quite fit. */
    overflow: auto;
    box-sizing: border-box;
    -webkit-overflow-scrolling: touch;
}

#calendar.fullscreen h1 {
    display: none;
}

#calendar.fullscreen .calendar-fullscreen-btn {
    /* Pin to the viewport corner so it doesn't pan away with the years. */
    position: fixed;
    top: max(8px, env(safe-area-inset-top));
    right: max(8px, env(safe-area-inset-right));
}

/* The vertical SVG pans; don't center it like the normal grid. */
#calendar.fullscreen > svg {
    margin: 0;
}

/* Freeze the page behind the lightbox (same as network-fullscreen-open). */
body.calendar-fullscreen-open {
    overflow: hidden;
}

/* Center the calendar grid SVG under the "Quartet Days" title.
   `> svg` is intentional — the legend SVG lives inside .calendar-top and
   shouldn't be re-centered here. */
#calendar > svg {
    display: block;
    margin: 0 auto;
}

/* Calendar SVG text defaults. The d3-created text elements (year labels,
   day-of-week, per-year stats, month labels, legend ticks) don't set an
   explicit fill, so they'd render black-on-bg. Default them to primary text,
   and set `color` so any `currentColor` references (legend title) resolve
   the same way. Text elements with explicit fill (chart annotations using
   --color-text-chart) keep their attribute. */
#calendar svg {
    color: var(--color-text-primary);
}

#calendar svg text:not([fill]) {
    fill: var(--color-text-primary);
}

/* Legend SVG sized + positioned to track the calendar's first 10 day cells.
   The calendar SVG has natural width 1000 (viewBox) and max-width: 100%, so
   its 10 cells render at min(container * 17%, 170px) and start at
   min(container * 4.05%, 40.5px). Mirror that here. */
.calendar-top > svg {
    width: min(170px, 17%);
    margin-left: min(40.5px, 4.05%);
    height: auto;
    flex-shrink: 0;
}

.recent-stats {
    font: 10px sans-serif;
    flex: 1;
    min-width: 0;
}

.recent-stats h4 {
    /* +3px nudges the heading baseline to match the legend title's baseline. */
    margin: 3px 0 4px 0;
    font-size: 11px;
    font-weight: bold;
}

.recent-stats-row {
    display: flex;
    flex-wrap: wrap;
    gap: 16px;
}

.recent-stat {
    display: inline-flex;
    gap: 4px;
}

.recent-stat-label {
    color: var(--color-text-secondary);
}

/* Full labels by default; the dashboard-breakpoint media query below swaps
   in the short ones on phones (both spans are rendered by renderRecentStats). */
.recent-stat-label--short {
    display: none;
}

.recent-stat-value {
    font-weight: bold;
}

/* ALL tab: aggregate stats over the current Date/Part/Player filter. */
.all-stats {
    font: 12px sans-serif;
    margin: 8px 0;
}

.all-stats-row {
    display: flex;
    flex-wrap: wrap;
    gap: 18px;
}

.all-stat {
    display: inline-flex;
    gap: 4px;
}

.all-stat-label {
    color: var(--color-text-secondary);
}

/* Full labels by default; the mobile media query below swaps in the short
   ones (both spans are rendered by updateAllTabContent), mirroring the
   calendar's recent-stats row. */
.all-stat-label--short {
    display: none;
}

.all-stat-value {
    font-weight: bold;
}

/* ---------- Dashboard view ---------- */

#dashboard {
    max-width: 1000px;
    margin: 0 auto;
    padding: 0 10px;
    box-sizing: border-box;
}

#dashboardDateFilter {
    margin: 10px 0;
}

.dashboard-charts {
    display: flex;
    flex-direction: column;
    gap: 18px;
    margin-top: 10px;
}

.dashboard-section {
    width: 100%;
}

.dashboard-chart-title {
    font: bold 13px sans-serif;
    color: var(--color-text-primary);
    margin: 0 0 6px 0;
}

.dashboard-empty {
    font: 12px sans-serif;
    color: var(--color-text-tertiary);
    margin: 6px 0;
}

/* KPI stat tiles at the top of the dashboard. Content-sized (widest of the
   label and a ~4-digit value), wrapping when they don't fit. margin-left is
   set inline by renderStats() to the ranked charts' plot-area left edge
   (sizing().rankedMargin.left) so the row aligns with the bars below, not
   the screen edge. */
.stat-tiles {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}

.stat-tile {
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    gap: 1px;
    background: var(--color-bg-secondary);
    border: 1px solid var(--color-border-subtle);
    border-radius: 8px;
    padding: 7px 11px;
    box-sizing: border-box;
    -webkit-tap-highlight-color: transparent;
}

.stat-tile-label {
    font: 11px sans-serif;
    color: var(--color-text-secondary);
    white-space: nowrap;
}

/* Proportional figures (the font default) on purpose: tabular-nums pads
   digits to '0' width, which looks loose at display sizes. */
.stat-tile-value {
    font: 600 22px sans-serif;
    color: var(--color-text-primary);
    line-height: 1.2;
}

/* Tonight's contribution to the tile above it, on the confirmation screen.
   The dashboard's tile with one more line, rather than a second tile: a
   lifetime total on its own says nothing about the piece just logged. */
.stat-tile-delta {
    font: 600 11px sans-serif;
    color: var(--color-text-success);
}

.stat-tile-delta--flat {
    color: var(--color-text-tertiary);
}

/* Mobile (mirrors MOBILE_BREAKPOINT in dashboardComponent.js): all six
   tiles share one full-width line — equal flex shares, compressed type,
   ellipsis on labels for very narrow phones. renderStats() drops the
   plot-area indent at this width. The calendar's "Last 365 days" row and
   the ALL tab's stats row also swap to their short labels at this same
   breakpoint. */
@media only screen and (max-width: 599px) {
    .stat-tiles {
        flex-wrap: nowrap;
        gap: 6px;
    }

    /* Tighter padding + smaller value than the four-tile layout used, so a
       four-digit value (the All range) still clears the tile border when
       six share the row. The vw cap keeps that true below ~390px: each
       tile's content box is (100vw - 50px)/6 - 10px (page padding, five
       6px gaps, border + padding), and four 600-weight digits run ~2.3ch,
       so 4.5vw stays inside it down to 320px where 16px no longer does. */
    .stat-tile {
        flex: 1 1 0;
        min-width: 0;
        padding: 6px 4px;
    }

    .stat-tile-label {
        font-size: 10px;
        overflow: hidden;
        text-overflow: ellipsis;
    }

    .stat-tile-value {
        font-size: min(16px, 4.5vw);
    }

    .recent-stat-label--long {
        display: none;
    }

    .recent-stat-label--short {
        display: inline;
    }

    .all-stat-label--long {
        display: none;
    }

    .all-stat-label--short {
        display: inline;
    }
}

/* SVGs inside the dashboard share the same font as the rest of the app */
#dashboardPartBar svg,
#dashboardComposerChart svg,
#dashboardMusicianChart svg,
#dashboardMusicianNetworkGraph svg,
#dashboardMusicianNetworkMatrix svg,
#dashboardMusicianNetworkChord svg {
    font-family: sans-serif;
}

/* Reserve room for the full TOP_N rows so filter toggles (and the empty
   "no data" state) don't change the chart container's height — otherwise
   the network section below would scroll up/down with every filter click.
   Keep in sync with TOP_N=20 + rankedRowHeight in src/dashboardComponent.js. */
#dashboardComposerChart,
#dashboardMusicianChart {
    min-height: 456px; /* desktop: 20 * 22 + 8 + 8 */
}
@media (max-width: 599px) {
    #dashboardComposerChart,
    #dashboardMusicianChart {
        min-height: 652px; /* mobile: 20 * 32 + 6 + 6 */
    }
}

/* Pull-to-refresh indicator. Built by src/pullToRefresh.js and only
   appended to the DOM when running as an installed PWA (standalone
   display mode). The spinner sits just below the top safe-area inset,
   translated up by its own height by default; --ptr-y (set in JS during
   a pull) translates it back down so it appears to be pulled out of the
   top of the screen. transitions are disabled via .pulling so it tracks
   the finger 1:1, then re-enabled for the snap-to-refresh / release. */
#ptr-indicator {
    position: fixed;
    top: env(safe-area-inset-top, 0);
    left: 50%;
    width: 36px;
    height: 36px;
    margin-left: -18px;
    border-radius: 50%;
    background: var(--color-bg-primary);
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 2000;
    pointer-events: none;
    transform: translateY(calc(-100% + var(--ptr-y, 0px)));
    opacity: var(--ptr-opacity, 0);
    transition: transform 0.2s ease-out, opacity 0.2s ease-out;
}
#ptr-indicator.pulling {
    transition: none;
}
#ptr-indicator .ptr-spinner {
    width: 18px;
    height: 18px;
    border: 2px solid var(--color-border-strong);
    border-top-color: var(--color-accent);
    border-radius: 50%;
}
#ptr-indicator.refreshing .ptr-spinner {
    animation: ptr-spin 0.8s linear infinite;
}
@keyframes ptr-spin {
    to { transform: rotate(360deg); }
}

/* Reserve room for the tallest network view (chord on desktop, graph on
   mobile) so switching tabs doesn't shift the page. Inactive views are
   display:none so they don't double-count. Keep in sync with sizing() in
   src/musicianNetworkComponent.js (graphHeight, chordDiameter). */
.network-view {
    min-height: 500px; /* desktop: max(graphHeight 460, chordDiameter 500) */
}
@media (max-width: 599px) {
    .network-view {
        min-height: 380px; /* mobile: max(graphHeight 380, chordDiameter 340) */
    }
}

/* Musician network: tab toggle + min-sessions slider + view containers. */
.network-controls {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 14px;
    margin-bottom: 8px;
    /* Match the visualization's MAX_DESIGN_WIDTH from
       musicianNetworkComponent.js so the right-aligned fullscreen button
       lines up with the graph view's right edge instead of the section's. */
    max-width: 720px;
}

#dashboardMusicianNetwork.fullscreen .network-controls {
    max-width: none;
}

.network-threshold {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    font: 12px sans-serif;
    color: var(--color-text-secondary);
}

.network-threshold-value {
    color: var(--color-text-primary);
    font-weight: 600;
    min-width: 1.5em;
    display: inline-block;
}

.network-threshold input[type="range"] {
    width: 140px;
    accent-color: var(--color-accent);
}

.network-fullscreen-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: var(--color-bg-primary);
    border: 1px solid var(--color-border-strong);
    border-radius: 6px;
    cursor: pointer;
    padding: 5px 9px;
    color: var(--color-text-primary);
    /* Push to the right edge of the flex controls row so it sits opposite
       the tabs/slider instead of immediately to the right of them. */
    margin-left: auto;
    -webkit-tap-highlight-color: transparent;
}

.network-fullscreen-btn:hover {
    background: var(--color-bg-hover);
}

.network-fullscreen-btn svg {
    pointer-events: none;
}

/* Lightbox mode: the section fills the viewport. The tabs / slider / caption
   stay in place; the active view's SVG grows because render() re-measures
   the container and re-runs the force layout at the new size. On phones the
   win is smaller than on desktop but still real (landscape rotation, no
   surrounding page chrome), so the button shows at every viewport width. */
#dashboardMusicianNetwork.fullscreen {
    position: fixed;
    top: 0;
    left: 0;
    width: 100vw;
    height: 100vh;
    /* Mobile Safari/Chrome: 100vh includes the collapsed URL-bar area; dvh
       tracks the actual visible viewport so the caption row isn't pushed
       under the browser chrome. Older engines keep the 100vh fallback. */
    height: 100dvh;
    z-index: 1000;
    background: var(--color-bg-primary);
    /* 20px design padding, expanded on standalone iOS to clear the status bar
       / home indicator / landscape notches. max() picks whichever is larger
       per side so notched devices don't lose visual breathing room. */
    padding-top: max(20px, env(safe-area-inset-top));
    padding-right: max(20px, env(safe-area-inset-right));
    padding-bottom: max(20px, env(safe-area-inset-bottom));
    padding-left: max(20px, env(safe-area-inset-left));
    overflow: auto;
    box-sizing: border-box;
}

/* While the lightbox is open, freeze the page behind it so touch scrolling
   inside the overlay (matrix pan, overflow) doesn't drag the dashboard
   around underneath. Toggled by _toggleFullscreen(). */
body.network-fullscreen-open {
    overflow: hidden;
}

.network-tabs {
    display: inline-flex;
    border: 1px solid var(--color-border-strong);
    border-radius: 6px;
    overflow: hidden;
    background: var(--color-bg-primary);
}

.network-tab-btn {
    padding: 6px 14px;
    background: var(--color-bg-primary);
    border: none;
    border-right: 1px solid var(--color-border-strong);
    cursor: pointer;
    font: inherit;
    color: var(--color-text-primary);
    transition: background-color 0.15s, color 0.15s;
    -webkit-tap-highlight-color: transparent;
}

.network-tab-btn:last-child {
    border-right: none;
}

.network-tab-btn:hover {
    background: var(--color-bg-hover);
}

.network-tab-btn.active {
    background: var(--color-accent);
    color: var(--color-text-light);
}

.network-view svg .network-node,
.network-view svg .network-edge,
.network-view svg .matrix-cell,
.network-view svg .matrix-label,
.network-view svg .network-arc,
.network-view svg .network-chord {
    cursor: pointer;
}

/* Chord arc labels match the graph-node label halo so they stay legible
   over ribbons in both themes. */
.network-view svg .network-arc-label {
    fill: var(--color-text-primary);
    pointer-events: none;
    paint-order: stroke;
    stroke: var(--color-bg-primary);
    stroke-width: 3px;
    stroke-linejoin: round;
}

/* Halo around graph labels so they stay legible over edges in both themes
   without any JS color reads. */
.network-view svg .network-label {
    fill: var(--color-text-primary);
    pointer-events: none;
    paint-order: stroke;
    stroke: var(--color-bg-primary);
    stroke-width: 3px;
    stroke-linejoin: round;
}

/* Matrix can exceed container width on narrow viewports; let it scroll
   horizontally rather than shrinking node-set parity with the graph. */
#dashboardMusicianNetworkMatrix {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
}

.network-caption {
    font: 11px sans-serif;
    color: var(--color-text-tertiary);
    margin: 6px 0 0 0;
    flex: 1;
}

/* Caption sits to the left, "Names" toggle on the right. Same max-width
   as the controls row so it lines up with the visualization's right edge. */
.network-caption-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 12px;
    max-width: 720px;
}

#dashboardMusicianNetwork.fullscreen .network-caption-row {
    max-width: none;
}

.network-show-names {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    font: 11px sans-serif;
    color: var(--color-text-secondary);
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    white-space: nowrap;
}

.network-show-names input[type="checkbox"] {
    margin: 0;
    accent-color: var(--color-accent);
    cursor: pointer;
}

.work-row {
    display: grid;
    grid-template-columns: auto 1fr;
    align-items: center;
    /* Breathing room between the label and the first play square. Labels
       stay content-width (ragged) on purpose: fixed-column alignment would
       waste most of the row on tabs with one long title (e.g. 5+'s
       "Strauss-Capriccio sextet"). */
    column-gap: 4px;
}

/* MISC tab needs wider first column for composer-prefixed titles */
#MISC .work-row {
    grid-template-columns: 130px 1fr;
}

.work-label-container {
    margin-left: 10px;
    min-width: 50px;
    white-space: nowrap;
}

.squares-container {
    display: flex;
    align-items: center;
    overflow-x: auto; /* In case of many squares */
}

.play-square {
    width: 10px;
    height: 10px;
    margin-right: 2px;
}

@media only screen and (max-width: 800px) { /* iPhone screen size (2x pixels)*/
    /* Adjust styles for smaller screens */
    body {
        font-size: 12px;
    }
    .tab {
        font-size: 12px;
    }
}

/* Player multiselect dropdown */
.player-multiselect {
    position: relative;
    display: inline-block;
}

.player-select-trigger {
    padding: 4px 8px;
    border: 1px solid var(--color-border-strong);
    background: var(--color-bg-primary);
    cursor: pointer;
    min-width: 100px;
    user-select: none;
}

.player-select-trigger:hover {
    background: var(--color-bg-stripe);
}

.player-dropdown {
    display: none;
    position: absolute;
    top: 100%;
    left: 0;
    background: var(--color-bg-primary);
    border: 1px solid var(--color-border-strong);
    /* max-height is set on open by NavigationComponent.fitDropdownHeight --
       the room under the trigger is not something CSS can ask about. This is
       the fallback for the first paint and for a JS-less render. */
    max-height: 300px;
    overflow-y: auto;
    z-index: 100;
    min-width: 150px;
    box-shadow: 0 2px 4px var(--color-shadow-light);
}

.player-dropdown.open {
    display: block;
}

.player-option {
    /* Matched to the work rows on Home (.work-row, 14px at the same 12px
       font): this list is read the same way and sat 10px taller per row. The
       floor is the checkbox's own 13px box, so 2px is most of what is left
       to give. */
    padding: 2px 8px;
    cursor: pointer;
    display: flex;
    align-items: center;
    gap: 6px;
    white-space: nowrap;
}

/* The row's height was set by the checkbox's UA margin (3px top and bottom)
   rather than by the padding above it, which made every row 3px taller than
   the rule here claimed and left no way to tighten it from the rule. */
.player-option input[type="checkbox"] {
    margin: 0;
    flex: none;
}

.player-option:hover {
    background: var(--color-bg-hover);
}

.player-option label {
    cursor: pointer;
    flex: 1;
}

.player-clear {
    padding: 4px 8px;
    border-bottom: 1px solid var(--color-border-strong);
    color: var(--color-text-secondary);
    cursor: pointer;
    font-size: 0.9em;
}

.player-clear:hover {
    background: var(--color-bg-hover);
}

/* Setup view styles */
#setupView {
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    padding: 20px;
    box-sizing: border-box;
}

.setup-container {
    max-width: 600px;
    width: 100%;
    text-align: center;
}

.setup-container h1 {
    margin-bottom: 10px;
}

.setup-container p {
    color: var(--color-text-secondary);
    margin-bottom: 20px;
}

#setupForm {
    display: flex;
    flex-direction: column;
    gap: 15px;
}

#dataUrlInput {
    width: 100%;
    padding: 12px;
    font-size: 14px;
    border: 1px solid var(--color-border-strong);
    border-radius: 4px;
    box-sizing: border-box;
}

#dataUrlInput:focus {
    outline: none;
    border-color: var(--color-accent);
    box-shadow: 0 0 0 2px var(--color-accent-focus-ring);
}

#setupForm button {
    padding: 12px 24px;
    font-size: 16px;
    background-color: var(--color-accent);
    color: var(--color-text-light);
    border: none;
    border-radius: 4px;
    cursor: pointer;
    transition: background-color 0.2s;
}

#setupForm button:hover {
    background-color: var(--color-accent-hover);
}

/* Secondary action inside the setup form — outlined, lower visual weight
   than the primary "Load Data" CTA. Used by the Copy-mobile-setup-link
   button. */
#setupForm button.setup-secondary {
    background-color: transparent;
    color: var(--color-accent);
    border: 1px solid var(--color-accent);
}

#setupForm button.setup-secondary:hover {
    background-color: var(--color-bg-hover);
}

.setup-error {
    color: var(--color-text-error);
    font-size: 14px;
    text-align: left;
    display: none;
}

.setup-help {
    margin-top: 20px;
    font-size: 14px;
}

.setup-help a {
    color: var(--color-accent);
    text-decoration: none;
}

.setup-help a:hover {
    text-decoration: underline;
}

/* ============================================================================
   Log form (#log)

   A label column and a field column: every row reads left-to-right as
   "what this is" then "what it says", and the eye scans one edge instead of
   two. The grid collapses to stacked rows on phones, where the form is
   actually used. Inputs are 16px so iOS Safari doesn't zoom the page on
   focus — smaller text there costs a viewport reset on every tap.
   ========================================================================= */
.log-form {
    display: grid;
    grid-template-columns: max-content minmax(0, 1fr);
    gap: 7px 14px;
    align-items: center;
    max-width: 560px;
    margin: 0 auto;
    padding: 0 16px 24px;
}

.log-field {
    display: contents;
}

/* An element with its own `display` outranks the UA's [hidden] rule, so the
   form/setup toggle has to say it again with the attribute in the selector. */
.log-form[hidden],
.log-setup[hidden],
.log-proposal[hidden] {
    display: none;
}

/* A ?form= link asking to redirect where entries go. Weighted as a warning,
   because accepting it silently is the failure the per-user config exists to
   prevent, and the safe choice is the prominent one. */
.log-proposal {
    max-width: 560px;
    margin: 0 auto;
    padding: 16px;
    border: 1px solid var(--color-text-error);
    border-radius: 8px;
}

.log-proposal p {
    margin: 0 0 14px;
}

.log-proposal code {
    color: var(--color-text-primary);
}

/* The form ids in the warning are links to the forms themselves. Underlined
   rather than only recoloured: this is the one place the id is not decoration
   but the thing to go and check, and inside a <code> run a colour shift alone
   reads as syntax highlighting. */
.log-proposal code a,
.log-connected code a {
    color: inherit;
    text-decoration: underline;
}

.log-proposal-actions {
    display: flex;
    align-items: center;
    gap: 14px;
}

.log-proposal-actions .log-submit {
    flex: 1;
    margin-top: 0;
}

.log-field label {
    color: var(--color-text-secondary);
    justify-self: end;
    white-space: nowrap;
}

.log-field input,
.log-field select {
    width: 100%;
    padding: 8px 10px;
    font: inherit;
    font-size: 16px;
    color: var(--color-text-primary);
    background: var(--color-bg-primary);
    border: 1px solid var(--color-border-standard);
    border-radius: 6px;
    box-sizing: border-box;
}

.log-field input:focus,
.log-field select:focus {
    outline: none;
    border-color: var(--color-accent);
    box-shadow: 0 0 0 3px var(--color-accent-focus-ring);
}

/* A required field the submit found empty. Named in the status line too, but
   on a phone the empty one can be three fields up the screen. */
.log-field input.is-missing,
.log-field select.is-missing,
#logPart.is-missing {
    border-color: var(--color-text-error);
}

/* A literal rgba like --color-accent-focus-ring, not color-mix(): the bundle
   targets Safari 15.4 and color-mix() lands in 16.2. */
#logPart.is-missing {
    box-shadow: 0 0 0 3px var(--color-error-focus-ring);
}

/* markMissing puts the cursor in the first empty field, so the focus ring and
   the error border land on the same control. Without this it draws the accent
   ring around a red border, which is two signals disagreeing at exactly the
   moment the user needs one. */
.log-field input.is-missing:focus,
.log-field select.is-missing:focus {
    border-color: var(--color-text-error);
    box-shadow: 0 0 0 3px var(--color-error-focus-ring);
}

/* The carried-forward value shows here, so it has to read as a real name
   that WILL be recorded rather than as absent text. */
.log-field input::placeholder {
    color: var(--color-text-tertiary);
    opacity: 1;
}

/* Extra controls in an already-occupied field column: the value column, in
   whichever grid is in force — a negative line counts back from the explicit
   grid's end, so it can never mint an implicit one. Column 2 under the desktop
   `max-content minmax(0, 1fr)`, column 1 under the phone's single track, and
   no second declaration to keep in step with this one. A literal `2` needed
   that phone override, and when the override's list of ids drifted from this
   rule's — #logComposer was here and not there — revealing the catalog picker
   minted the implicit second column, collapsing the auto-placed column to 0px
   and crushing every label, input and note in the form into it. */
.log-field-extra {
    grid-column: -2;
}

/* Others?: a row per person, each the same name-plus-part pair as a seat. */
.log-others {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.log-other-row {
    display: flex;
    gap: 6px;
}

.log-other-row input {
    flex: 1;
    min-width: 0;
}

.log-field .log-other-row select {
    width: auto;
    flex-shrink: 0;
    padding: 8px 6px;
    font-size: 15px;
    color: var(--color-text-secondary);
}

.log-other-drop {
    flex-shrink: 0;
    padding: 0 10px;
    font: inherit;
    color: var(--color-text-tertiary);
    background: transparent;
    border: 1px solid var(--color-border-standard);
    border-radius: 6px;
    cursor: pointer;
}

.log-other-drop:hover {
    color: var(--color-destructive);
}

.log-others-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}

/* Lower weight than the rows above it: the exception, not the path. */
#logOthersFree {
    font-size: 14px;
}

#logOthersHere:empty {
    display: none;
}

.log-chip-btn--here {
    padding: 5px 10px;
    font-size: 14px;
    color: var(--color-text-secondary);
    border-style: dashed;
}

/* Name plus the part they played. The part is a secondary attribute changed
   occasionally, so it sits beside the name at its natural width rather than
   taking a row of its own. */
.log-slot {
    display: flex;
    gap: 6px;
}

.log-slot input {
    flex: 1;
    min-width: 0;
}

.log-field .log-slot-part {
    width: auto;
    flex-shrink: 0;
    padding: 8px 6px;
    font-size: 15px;
    color: var(--color-text-secondary);
}

/* One-tap composers. Wraps rather than scrolls: seeing all of them at once is
   the whole advantage over the picker. */
.log-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
}

.log-chip-btn {
    padding: 7px 12px;
    font: inherit;
    font-size: 15px;
    color: var(--color-text-primary);
    background: var(--color-bg-primary);
    border: 1px solid var(--color-border-standard);
    border-radius: 999px;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: background-color 0.15s, color 0.15s;
}

.log-chip-btn:hover {
    background: var(--color-bg-hover);
}

.log-chip-btn.active {
    color: var(--color-text-light);
    background: var(--color-accent-selected);
    border-color: var(--color-accent-selected);
}

/* The escape to the full catalog reads as a control, not as a composer. */
.log-chip-btn--more {
    color: var(--color-accent);
    border-style: dashed;
    border-color: var(--color-accent);
}

.log-chips.is-missing .log-chip-btn {
    border-color: var(--color-text-error);
}

#logPart {
    justify-self: start;
}

.log-note,
.log-preview,
.log-status,
.log-submit,
.log-pending-title {
    grid-column: 1 / -1;
}

/* The row the fields will become, columns separated the way the sheet
   separates them. Quiet: it is a receipt, not a control. Monospace so the
   four cells stay four cells when a name is long. */
/* What the row does NOT include, when the form left somebody out. A step
   louder than the row itself: it is the one thing on this line that the
   logger may need to act on. */
.log-preview-note {
    display: block;
    margin-top: 4px;
    /* Named, not inherited: this span sits inside .log-preview, which sets a
       monospace stack for the row itself, and this is a sentence not a row. */
    font-family: sans-serif;
    font-style: italic;
    color: var(--color-text-secondary);
}

.log-preview {
    margin: 6px 0 0;
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
    font-size: 12px;
    line-height: 1.5;
    color: var(--color-text-tertiary);
    overflow-wrap: break-word;
}

.log-note {
    margin: 2px 0 0;
    font-size: 13px;
    color: var(--color-text-tertiary);
}

/* The literal the note is about is a single hyphen, which reads as
   punctuation unless it's given an edge. */
.log-note code {
    padding: 1px 5px;
    color: var(--color-text-secondary);
    background: var(--color-bg-secondary);
    border-radius: 3px;
}

/* One-tap re-entry of the previous row's Others?, which is the only column
   that doesn't carry itself forward. Low weight: it's an offer, not a state. */
.log-chip {
    justify-self: start;
    width: auto;
    padding: 4px 10px;
    font: inherit;
    font-size: 13px;
    color: var(--color-accent);
    background: transparent;
    border: 1px dashed var(--color-accent);
    border-radius: 999px;
    cursor: pointer;
}

.log-chip:hover {
    background: var(--color-bg-hover);
}

.log-submit {
    margin-top: 10px;
    padding: 12px;
    font: inherit;
    font-size: 16px;
    color: var(--color-text-light);
    background: var(--color-accent);
    border: none;
    border-radius: 6px;
    cursor: pointer;
    transition: background-color 0.2s;
}

.log-submit:hover:not(:disabled) {
    background: var(--color-accent-hover);
}

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

.log-status {
    min-height: 1.2em;
    margin: 0;
    font-size: 14px;
    color: var(--color-text-secondary);
}

.log-status.ok { color: var(--color-text-success); }
.log-status.error { color: var(--color-text-error); }

/* Connect-your-form panel, shown in place of the fields until this device
   knows where to write. */
.log-setup {
    max-width: 560px;
    margin: 0 auto;
    padding: 0 16px 24px;
}

.log-setup ol {
    margin: 10px 0;
    padding-left: 22px;
    color: var(--color-text-secondary);
    line-height: 1.6;
}

.log-setup input {
    width: 100%;
    padding: 8px 10px;
    font: inherit;
    font-size: 16px;
    color: var(--color-text-primary);
    background: var(--color-bg-primary);
    border: 1px solid var(--color-border-standard);
    border-radius: 6px;
    box-sizing: border-box;
}

.log-setup .log-submit {
    display: block;
    width: 100%;
}

.log-setup-help {
    margin: 14px 0 0;
    font-size: 13px;
    color: var(--color-text-tertiary);
}

.log-setup-help a {
    color: var(--color-accent);
}

/* The parsed mapping, shown before saving. Ids map to columns positionally,
   which is right by construction for a form and the sheet it created and
   wrong if the questions were later reordered — this is the one moment
   anyone can catch that. */
.log-setup-map:not(:empty) {
    margin: 12px 0;
    border: 1px solid var(--color-border-subtle);
    border-radius: 6px;
}

.log-setup-row {
    display: flex;
    justify-content: space-between;
    gap: 12px;
    padding: 4px 10px;
    font-size: 13px;
}

.log-setup-row:nth-child(odd) {
    background: var(--color-bg-stripe);
}

.log-setup-col {
    color: var(--color-text-secondary);
}

.log-setup-row code,
.log-connected code {
    color: var(--color-text-primary);
}

/* Which form this device writes through. Low weight, but present: "am I
   writing to my own sheet" should be answerable without opening devtools. */
.log-connected {
    grid-column: 1 / -1;
    margin: 4px 0 0;
    font-size: 12px;
    color: var(--color-text-tertiary);
    text-align: center;
}

.log-connected a {
    color: var(--color-accent);
}

/* The line under the button and its (i). They share a row so the sentence can
   stay short enough for a phone: the states that need more than a sentence put
   the rest behind the button instead of into the line. */
.log-status-line {
    grid-column: 1 / -1;
    display: flex;
    align-items: flex-start;
    gap: 6px;
}

.log-status-line .log-status {
    flex: 1;
    min-width: 0;
}

/* 44px of tap target around a 16px glyph, pulled back into the line's own
   leading so the affordance costs no height. */
.log-info {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    margin: -13px -13px -13px 0;
    padding: 0;
    color: var(--color-text-secondary);
    background: transparent;
    border: none;
    border-radius: 50%;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}

.log-info svg {
    width: 16px;
    height: 16px;
}

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

/* display outranks the UA's [hidden] rule, as with .log-form. */
.log-info[hidden] {
    display: none;
}

/* Expands in place, below the line. Not a floating tooltip: there is no
   positioning to get wrong at any width, and it is reachable by keyboard. */
.log-status-help {
    grid-column: 1 / -1;
    margin: 6px 0 0;
    padding: 11px 13px;
    font-size: 13px;
    line-height: 1.55;
    color: var(--color-text-secondary);
    background: var(--color-bg-secondary);
    border-radius: 7px;
}

/* =========================================================================
   The confirmation screen (#logDone)

   A centred column the same width as the form it replaces, top-aligned where
   the form was, and FLOWING — nothing is pinned to the viewport, so one
   layout serves a phone and a desktop. Pinning the actions to the bottom is
   right at 390px and wrong at 1440, where it stretches the panel and strands
   them at the foot of the window.
   ========================================================================= */
.log-done {
    display: flex;
    flex-direction: column;
    align-items: center;
    max-width: 560px;
    margin: 0 auto;
    padding: 0 16px 24px;
    box-sizing: border-box;
}

.log-done[hidden] {
    display: none;
}

.log-done-mark {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 52px;
    height: 52px;
    margin-top: 30px;
    color: var(--color-success-contrast);
    background: var(--color-text-success);
    border-radius: 50%;
}

.log-done-mark svg {
    width: 28px;
    height: 28px;
}

.log-done-what {
    margin: 13px 0 0;
    font-size: 27px;
    font-weight: normal;
    line-height: 1.15;
    text-align: center;
}

.log-done-who {
    margin: 5px 0 0;
    font-size: 15px;
    line-height: 1.45;
    color: var(--color-text-secondary);
    text-align: center;
}

.log-done-where {
    margin: 5px 0 0;
    font-size: 14px;
    color: var(--color-text-tertiary);
    text-align: center;
}

.log-done-block {
    width: 100%;
    margin-top: 22px;
    padding-top: 14px;
    border-top: 1px solid var(--color-border-subtle);
}

.log-done-sub {
    margin: 0 0 6px;
    font-size: 13px;
    color: var(--color-text-secondary);
}

.log-done-row {
    display: flex;
    align-items: center;
    gap: 9px;
    padding: 4px 0;
    font-size: 14px;
}

.log-done-when {
    flex-shrink: 0;
    width: 50px;
    font-size: 13px;
    color: var(--color-text-tertiary);
}

.log-done-piece {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.log-done-row--past .log-done-piece {
    color: var(--color-text-secondary);
}

/* A movement rather than a whole piece. The sheet keeps it and this app drops
   it, so it is in the sitting but not in the charts or the tiles below —
   italic is what says the dot beside it is answering a different question
   ("did it get sent") from the one on every other row. */
.log-done-row--partial .log-done-piece {
    font-style: italic;
}

.log-done-part {
    color: var(--color-text-tertiary);
}

/* Filled once the app's own copy of the sheet holds the row, hollow until
   then. Watching one fill in is what teaches the delay; a sentence about
   published copies never has. Both carry a text label for the same reason the
   part colours do — a state is never colour and shape alone. */
.log-done-dot {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 13px;
    height: 13px;
    border-radius: 50%;
    box-sizing: border-box;
}

.log-done-dot--landed {
    color: var(--color-success-contrast);
    background: var(--color-text-success);
}

.log-done-dot--waiting {
    border: 2px solid var(--color-success-pending);
}

/* An empty ring: the tick is what says the app's own copy holds the row. */
.log-done-dot--waiting svg {
    display: none;
}

.log-done-dot svg {
    width: 9px;
    height: 9px;
}

/* The dashboard tiles are content-sized and left-bunched, which is right in a
   full-width header and wrong in a 560px column; four of them share it evenly
   here. The phone rule already says flex: 1 1 0, so this is the same shape at
   both widths rather than a second layout. */
.log-done-tiles {
    width: 100%;
}

.log-done-tiles .stat-tile {
    flex: 1 1 0;
    min-width: 0;
}

/* The one warning there is: a partial movement, which the sheet keeps and
   this app hides — so it is said on the screen whose sitting list and tiles
   are about to leave the piece out. */
.log-done-warn {
    width: 100%;
    margin: 18px 0 0;
    font-size: 13px;
    line-height: 1.5;
    color: var(--color-text-secondary);
}

.log-done-warn[hidden] {
    display: none;
}

/* Long labels by default, the app's short forms where the tile row is tight
   — the same swap the calendar's stats row makes, done in CSS so no width is
   read in JS and no resize listener is needed. Both halves live here rather
   than in the dashboard's mobile block far above: same specificity, and the
   later rule wins, so an override up there would lose to this one. The
   breakpoint matches the .stat-tile mobile rule it shares a row with. */
.log-done-tiles .stat-tile-label--short {
    display: none;
}

@media only screen and (max-width: 599px) {
    .log-done-tiles .stat-tile-label--long {
        display: none;
    }

    .log-done-tiles .stat-tile-label--short {
        display: inline;
    }
}

.log-done-note {
    width: 100%;
    margin: 18px 0 0;
    font-size: 13px;
    line-height: 1.5;
    color: var(--color-text-tertiary);
}

.log-done-actions {
    display: flex;
    flex-direction: column;
    gap: 9px;
    width: 100%;
    margin-top: 22px;
}

.log-done-actions .log-submit {
    margin-top: 0;
}

.log-done-secondary {
    padding: 13px;
    font: inherit;
    font-size: 15px;
    color: var(--color-text-secondary);
    background: var(--color-bg-primary);
    border: 1px solid var(--color-border-standard);
    border-radius: 6px;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}

.log-done-secondary:hover {
    background: var(--color-bg-hover);
}

/* Queued entries. Visible and discardable on purpose: an invisible outbox is
   how a submission silently never happens. */
.log-pending {
    max-width: 560px;
    margin: 0 auto;
    padding: 12px 16px 32px;
    border-top: 1px solid var(--color-border-subtle);
}

.log-pending-title {
    margin: 0 0 8px;
    font-size: 13px;
    color: var(--color-text-secondary);
}

.log-pending-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    padding: 5px 0;
    font-size: 14px;
}

.log-pending-what {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.log-pending-drop {
    flex-shrink: 0;
    padding: 2px 9px;
    font: inherit;
    color: var(--color-destructive);
    background: transparent;
    border: 1px solid var(--color-border-standard);
    border-radius: 4px;
    cursor: pointer;
}

@media only screen and (max-width: 640px) {
    .log-form {
        grid-template-columns: minmax(0, 1fr);
        gap: 4px;
    }

    .log-field label {
        justify-self: start;
        margin-top: 8px;
        font-size: 13px;
    }
}
