:root {
    --color-primary: #000080;
    --color-primary-dark: #00004d;
    --color-primary-soft: #e6e6f2;
    --color-ink: #212529;
    --color-muted: #6c757d;
    --color-border: #e9ecef;
    --color-surface: #ffffff;
    --color-bg: #f6f7f9;
    --color-success: #2b8a3e;
    --color-success-soft: #ebfbee;
    --color-danger: #c1121f;
    --color-danger-soft: #fdecee;
    --color-pending: #495057;
    --color-pending-soft: #f1f3f5;

    /* Amber, for "this needs your attention" (Epic 45, docs/TASKS.md) - deliberately neither
       --color-danger (nothing has gone wrong and nothing was lost from the user's point of view;
       red would read as an error they caused) nor --color-success. Its only use today is the
       reconnect-your-account banner, which is a request for an action, not a report of a failure. */
    --color-warning: #a4620b;
    --color-warning-soft: #fff4e2;

    /* Live location (Epic 43, docs/TASKS.md) - the viewer's own position and track on the map. A
       vivid jade green, on request. Deliberately its own variable rather than reusing --color-success
       (#2b8a3e, a muted "this succeeded" green used by badges and by a Series item's "visited"
       styling): this marks *you*, not a state of your data, and the two must stay visually separable
       on the same map. map_controller.js's LOCATION_COLOR must be kept equal to --color-location by
       hand - a Stimulus controller can't read a custom property at module scope, and Leaflet needs a
       real colour string for its SVG stroke/fill. */
    --color-location: #00a94f;
    --color-location-dark: #008a40;

    /* The device's own heart rate, on geek mode's hrv chart (Epic 63, docs/TASKS.md). Red because that
       is what a heart rate is drawn in everywhere, and its *own* variable rather than --color-danger
       (#c1121f) for the same reason --color-location is not --color-success: this marks a measurement,
       not a failure, and the two must stay separable on a page where a real error can also appear. A
       brighter red than danger's, so a 2px line stays legible where it rides on the navy median. */
    --color-heart: #e5383b;

    --radius-sm: 6px;
    --radius: 10px;
    --shadow: 0 1px 4px rgba(20, 20, 30, 0.08);
    --shadow-lg: 0 8px 30px rgba(20, 20, 30, 0.12);

    --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}

* {
    box-sizing: border-box;
}

body {
    margin: 0;
    background: var(--color-bg);
    color: var(--color-ink);
    font-family: var(--font-sans);
    font-size: 15px;
    line-height: 1.5;
    -webkit-font-smoothing: antialiased;
    /* A column flex container so the shared footer (_footer.html.twig, rendered by base.html.twig for
       every page but the map) sits at the bottom of the viewport on a short page instead of riding up
       under the content, without needing position: fixed (which would overlap a long page's own
       content). Each page's top-level wrapper - .page or .auth-shell - is the one flex child that
       takes the remaining height. */
    display: flex;
    flex-direction: column;
    min-height: 100vh;
}

h1, h2, h3 {
    line-height: 1.25;
    font-weight: 600;
    margin: 0 0 12px;
}

h1 { font-size: 26px; }
h2 { font-size: 18px; margin-top: 32px; }

a {
    color: var(--color-primary);
    text-decoration: none;
}

a:hover {
    text-decoration: underline;
}

.page {
    max-width: 1240px;
    margin: 0 auto;
    padding: 20px 20px 60px;
    /* grow to push the footer down on a short page, but never shrink below the content's own height
       (basis auto, shrink 0) - a long workout list must scroll, not be compressed. */
    flex: 1 0 auto;
    /* Load-bearing, and a real regression caught in a browser rather than by reasoning: as a plain
       block this element's `width: auto` filled its parent up to max-width, but as a flex item in
       body's column layout, the `margin: 0 auto` that centers it is an *auto cross-axis margin*,
       which switches off `align-items: stretch` and leaves the item sized to fit-content. Every
       .page in the app visibly narrowed to its widest child. `width: 100%` restores the fill
       behavior; max-width still caps it and the auto margins still center it. */
    width: 100%;
}

/* Filter sidebar layout - list/import-log pages */
.layout {
    display: flex;
    align-items: flex-start;
    gap: 20px;
}

.layout .content {
    flex: 1;
    min-width: 0;
}

.filters {
    width: 240px;
    flex-shrink: 0;
    background: var(--color-surface);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    padding: 18px;
}

.filters h2 {
    margin: 0 0 14px;
    font-size: 14px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--color-muted);
}

/* General form field styling - originally scoped to .filters only (the workout/import-log list
   pages' sidebar), promoted to apply everywhere so every label/input/select/textarea in the app gets
   the same treatment without each page needing its own copy or its own opt-in. Genuinely global, not
   scoped to `form` - a `form`-ancestor requirement missed any field associated to its form via the
   HTML5 `form="..."` attribute instead of DOM nesting (e.g. series_items.html.twig's reorder-position
   inputs, which live in a <td> rather than inside the <form> they submit to - CSS's `form input`
   selector only understands DOM nesting, not the `form=` attribute), which is exactly backwards from
   the point of a *general* style: every text-like field should get it by default, not by remembering
   to nest it correctly. Checkbox/radio/file/hidden input types are excluded explicitly since "bordered,
   full-width, 13px" is a text-field treatment that would look broken applied to them (a hidden input
   doesn't render at all regardless, but the other three do). The map page's layer-toggle checkboxes
   (assets/controllers/map_controller.js's addBoundaryLayerCheckbox()) are the one place a bare `label`
   rule would reach that isn't a form field - `.layer-control label` below opts back out for that
   specific, deliberately different (compact, native-checkbox) context, the same targeted-override
   pattern `.row-action select` already uses for its own exception. */
label {
    display: block;
    font-size: 13px;
    font-weight: 600;
    color: var(--color-muted);
    margin-bottom: 4px;
}

input:not([type="hidden"]):not([type="file"]):not([type="checkbox"]):not([type="radio"]),
select,
textarea {
    width: 100%;
    padding: 8px 10px;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    background: var(--color-bg);
    font: inherit;
    font-size: 13px;
    color: var(--color-ink);
}

.field {
    margin-bottom: 14px;
}

.field-row {
    display: flex;
    gap: 8px;
}

.field-row .field {
    flex: 1;
    min-width: 0;
}

.filters .btn {
    width: 100%;
    justify-content: center;
    margin-top: 4px;
}

.filters .clear-filters {
    display: block;
    text-align: center;
    margin-top: 10px;
    font-size: 13px;
}

/* Native date inputs have a minimum intrinsic width their calendar icon pushes past, so two of them
   side by side in the 240px filter sidebar truncate the value ("06 / 08 / 2..." on a real screenshot).
   Stacked, each gets the full sidebar width. Scoped to `.filters` so the same .field-row keeps its
   two-column layout everywhere else, and living here beside .field-row itself rather than in the
   template - overriding a rule where the rule lives (CLAUDE.md's own rule of thumb). */
.filters .field-row { flex-direction: column; gap: 0; }
.filters .field-row .field { width: 100%; }

/* A monospace block for a format example - small enough that a scroll bar would be more intrusive
   than a wrap, so long lines wrap rather than overflow. */
.code-sample {
    background: var(--color-bg);
    border-radius: var(--radius-sm);
    padding: 12px 14px;
    font-size: 13px;
    white-space: pre-wrap;
    word-break: break-word;
    margin: 0 0 12px;
}

/* The workout list's left column: two separate cards stacked, filters then import actions. A column
   wrapper rather than restyling `.filters` itself, which four other pages use as a direct flex child
   of `.layout`. */
.sidebar {
    width: 240px;
    flex-shrink: 0;
    display: flex;
    flex-direction: column;
    gap: 16px;
}

.sidebar .filters { width: 100%; }

/* Same card chrome as `.filters`, and that's the point: a *separate* card reads as its own thing
   rather than as more filter controls, which is what was asked for. Full-width stacked buttons keep
   it from being mistaken for a row of form fields. */
.sidebar-card {
    background: var(--color-surface);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    padding: 18px;
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.sidebar-card h2 {
    margin: 0 0 4px;
    font-size: 14px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--color-muted);
}

.sidebar-card .btn { width: 100%; justify-content: center; }
.sidebar-note { margin: 4px 0 0; font-size: 13px; }

/* One row above the table: the pager on the left, the count and page-size chooser on the right.
   `margin-left: auto` rather than space-between, so the right-hand group stays right-aligned even on
   a single-page list where the pager renders nothing at all. */
.list-toolbar {
    display: flex;
    align-items: baseline;
    gap: 16px;
    flex-wrap: wrap;
    margin-bottom: 12px;
}

/* Both need the descendant selector, not a bare class: `.meta` and `.pager` are declared later in
   this file with their own `margin`, and at equal specificity source order wins - so a bare
   `.list-counts { margin-left: auto }` silently lost and the group sat left. Specificity rather than
   moving the rules, so it can't break again if either is reordered. */
.list-toolbar .pager { margin: 0; }
.list-toolbar .list-counts { margin: 0 0 0 auto; }
.page-size a, .page-size .page-size-current { margin-left: 6px; }
.page-size-current { font-weight: 600; color: var(--color-ink); }

@media (max-width: 780px) {
    .layout { flex-direction: column; }
    .filters { width: 100%; }
    .sidebar { width: 100%; }
    /* Back to two columns once the sidebar is full-width - there's room again, and stacked date
       fields would waste vertical space on the screen that has least of it. */
    .filters .field-row { flex-direction: row; gap: 8px; }
}

@media (max-width: 480px) {
    /* On one row the two halves collide at phone widths - measured at 390px, the page-size links ran
       off the right edge with "100" clipped. Stacked, each gets the full width. */
    .list-toolbar { flex-direction: column; align-items: flex-start; gap: 4px; }
    .list-toolbar .list-counts { margin-left: 0; }

}

/* Top nav - the same white/shadow/rounded/14px treatment as the map page's
   floating nav, just docked in normal document flow instead of positioned
   absolutely over a full-bleed canvas. */
.topbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    background: var(--color-surface);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    padding: 10px 18px;
    margin-bottom: 24px;
    font-size: 14px;
    flex-wrap: wrap;
}

.topbar-brand {
    display: flex;
    align-items: center;
    gap: 8px;
    font-weight: 700;
    font-size: 15px;
    color: var(--color-ink);
    letter-spacing: -0.01em;
}

.topbar-brand img {
    display: block;
    width: 28px;
    height: 28px;
    border-radius: 6px;
}

.topbar-brand:hover {
    text-decoration: none;
}

.topbar-links {
    display: flex;
    align-items: center;
    gap: 4px;
    flex-wrap: wrap;
}

.topbar-links a,
.topbar-links button {
    color: var(--color-muted);
    padding: 6px 10px;
    border-radius: var(--radius-sm);
    /* The logout link is a <button> now (see _nav.html.twig) - reset its
       default chrome so it matches the plain <a> links beside it. */
    font: inherit;
    background: none;
    border: none;
    cursor: pointer;
}

.topbar-links a:hover,
.topbar-links button:hover {
    background: var(--color-bg);
    color: var(--color-ink);
    text-decoration: none;
}

/* Deliberately no font-weight here (T20.8, docs/TASKS.md Epic 20) - it used to set font-weight: 600,
   which made the active link's rendered width differ from an inactive one, shifting every other link
   sideways in the flex row each time the active link changed. Removed rather than applied to every
   link instead, to keep every page's nav looking exactly as before except for the one previously-bold
   active link; color/background (the pill) alone now carries the active state. */
.topbar-links a.is-active {
    color: var(--color-primary);
    background: var(--color-primary-soft);
}

/* The logout <button> lives inside a <form> now (POST + CSRF, to stop
   browser prefetch from silently logging users out - see security.yaml) -
   display: contents makes the form itself invisible to flex layout, so the
   button lays out exactly like the sibling <a> links. */
.topbar-logout-form {
    display: contents;
}

/* Floating variant - used on the full-bleed map page, positioned over the
   canvas instead of sitting in normal document flow. */
.topbar--floating {
    position: absolute;
    top: 14px;
    right: 14px;
    z-index: 1000;
    margin-bottom: 0;
    width: auto;
}

/* T44.4 - once the nav wraps, the display name and the links sit on separate rows, and two details that
   are right on one row stop being right: the vertical divider after the name now separates nothing (it
   reads as a stray tick), and the links are indented relative to the name by their own tap-area
   padding, so two left edges that should line up don't. `is-stacked` is set by map_controller.js from
   the *measured* wrap (see updateNavLayout()) rather than a breakpoint - at the same width one display
   name wraps and a shorter one doesn't. The negative margin cancels exactly the horizontal padding
   `.topbar-links a` carries, so the link text aligns with the name's first letter rather than the
   button box aligning with it. */
/* **Both of these must stay paint-only - they must not change the nav's layout geometry.** The first
   version removed the border and zeroed the padding, and used a negative margin for the alignment. All
   three change the nav's size, and the class is applied from a *measurement* of that size, so at any
   width where dropping the divider was exactly enough to un-wrap the nav the two states chased each
   other: measured as wrapped -> divider removed -> now fits on one row -> measured as unwrapped ->
   divider back -> wraps again. Reported from production as the mobile map "twitching ~50 times per
   second"; reproduced at 430px as 180 class changes in 3 seconds (60Hz), with every other width stable
   at zero. A transparent border keeps the box identical, and a transform is applied after layout, so
   neither can feed back into the measurement. */
.topbar--floating.is-stacked .topbar-username {
    border-right-color: transparent;
}

.topbar--floating.is-stacked .topbar-links {
    transform: translateX(-10px);
}

/* T44 (docs/TASKS.md Epic 44) - narrow screens, from a real production phone screenshot: `width: auto`
   anchored only on the right meant that once the nav's content outgrew the viewport it grew *leftwards*
   off-screen, clipping the display name ("...ha Klepikov"). Anchoring both sides makes it wrap inside
   its own box instead. Kept here rather than in map/index.html.twig's own <style> block, where the base
   rule's `top`/`right` won on source order (AssetMapper injects this file after that inline block) and
   the override silently only half-applied - found by measuring the rendered box, not by reading it. */
@media (max-width: 640px) {
    .topbar--floating {
        left: 8px;
        right: 8px;
        top: 8px;
        padding: 6px 10px;
        gap: 8px;
        font-size: 13px;
    }

    .topbar--floating .topbar-links a,
    .topbar--floating .topbar-links button {
        padding: 4px 7px;
    }

    /* Matches the tighter link padding above - the offset has to equal whatever padding is in force. */
    .topbar--floating.is-stacked .topbar-links {
        transform: translateX(-7px);
    }

    /* Truncated rather than hidden: a long display name is the one piece of nav content with no upper
       bound, and on a /map/{uuid} share it names the map's owner, so it can't just be dropped on mobile
       the way a nav link could. */
    .topbar--floating .topbar-username {
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        max-width: 45vw;
    }
}

/* The map's own layer-toggle checkboxes (map_controller.js's addBoundaryLayerCheckbox()) sit outside
   any <form> and pair a native checkbox with its label inline ("[x] Grid"), a compact control-panel
   look rather than a form field - opts back out of the general `label` rule's block/bold/muted-gray
   treatment above, the same targeted-exception pattern `.row-action select` uses for its own case.
   `map/index.html.twig` already declares its own `.layer-control label` (display: flex; gap: 6px;
   cursor: pointer; white-space: nowrap;) in a page-level <style> block - `display: flex` is restated
   here (not left to whichever rule happens to load later) so this override can't fight it regardless
   of load order; `gap`/`cursor`/`white-space` are deliberately left undeclared here so that page's own
   rule keeps controlling them either way. */
.layer-control label {
    display: flex;
    font-size: inherit;
    font-weight: normal;
    color: inherit;
    margin-bottom: 0;
}

.topbar-username {
    font-weight: 600;
    padding-right: 10px;
    margin-right: 2px;
    border-right: 1px solid var(--color-border);
}

/* Shared site footer (_footer.html.twig) - the counterpart to .topbar, on every page except the
   full-bleed map, which overrides the `footer` block away. Deliberately not a .card: it's chrome
   around the page, so it spans the full width with its own inner element carrying .page's exact
   1240px max-width, keeping its text aligned with whatever content sits above it. */
.site-footer {
    /* Never absorb the leftover height body's flex layout is handing to the page wrapper above, and
       never compress on a long page. */
    flex-shrink: 0;
    background: var(--color-surface);
    border-top: 1px solid var(--color-border);
    color: var(--color-muted);
    font-size: 13px;
}

.site-footer-inner {
    max-width: 1240px;
    margin: 0 auto;
    padding: 12px 20px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    flex-wrap: wrap;
}

/* The ODbL credit and the Privacy Policy link travel together on the right, opposite the copyright -
   its own flex row so the pair wraps as a unit on a narrow viewport rather than the credit dropping
   to a line of its own while the link stays put. */
.site-footer-secondary {
    display: flex;
    align-items: center;
    gap: 16px;
    flex-wrap: wrap;
}

/* Muted rather than the global navy link color - a footer link is secondary chrome, and the navy
   would pull the eye away from the page's real content and actions. */
.site-footer a {
    color: var(--color-muted);
}

.site-footer a:hover {
    color: var(--color-primary);
}

/* A row of page-level call-to-action buttons/links - e.g. the workout list's "Fetch from Suunto" +
   "Upload a workout" pair. Deliberately a plain flex row, not a .card - these are actions, not
   content. `.btn` already renders consistently on both a <button> (a form submit, like the Suunto
   fetch) and an <a> (a plain navigation link, like Upload) - a page-actions row lets the two sit next
   to each other looking like matched buttons despite being different elements underneath. */
.page-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
    margin-bottom: 20px;
}

/* Cards */
.card {
    background: var(--color-surface);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    padding: 20px;
    margin-bottom: 20px;
}

.card-title {
    margin: 0 0 4px;
}

/* Constrains a long text document's own line length for readability - .card itself fills whatever
   width its container (.page, 1240px) gives it, too wide to read comfortably as running prose. */
.legal-content {
    max-width: 760px;
}

.legal-content h2:first-child {
    margin-top: 0;
}

.legal-content ul {
    padding-left: 20px;
}

.legal-content li {
    margin-bottom: 4px;
}

.meta {
    color: var(--color-muted);
    font-size: 14px;
    margin: 0 0 16px;
}

/* Buttons */
.btn {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font: inherit;
    font-size: 14px;
    font-weight: 600;
    border: 1px solid transparent;
    border-radius: var(--radius-sm);
    padding: 9px 16px;
    cursor: pointer;
    background: var(--color-surface);
    color: var(--color-ink);
    border-color: var(--color-border);
}

.btn:hover {
    text-decoration: none;
    border-color: #d0d4d9;
}

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

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

.btn-danger {
    background: var(--color-danger-soft);
    border-color: var(--color-danger);
    color: var(--color-danger);
}

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

/* A `<button type="submit">` that should read as a plain text link, not a boxed `.btn` - e.g.
   series_items.html.twig's "Remove", sitting inline next to plain "Preview"/"Edit" <a> links. A
   mutating action still has to be a form submit (CSRF), but visually it should match its neighbors
   rather than standing out as the one filled button in an otherwise plain-link row. */
.link-button {
    background: none;
    border: none;
    padding: 0;
    font: inherit;
    font-size: inherit;
    cursor: pointer;
    color: var(--color-primary);
}

.link-button:hover {
    text-decoration: underline;
}

.link-button.link-danger {
    color: var(--color-danger);
}

/* workout/show.html.twig's "Exclude from geo stats" toggle - rarely clicked, so it's deliberately
   styled to read as a minor, easy-to-ignore action rather than the boldest link on the page (it used
   to sit in the details table looking like the primary action, on request de-emphasized instead). */
.link-button.link-muted {
    color: var(--color-muted);
}

.link-button.link-muted:hover {
    color: var(--color-ink);
}

/* A <form> that should lay out as a plain inline sibling among <a> links (e.g. series_items.html.twig's
   Preview/Edit/Remove row) rather than as its own block - the same display:contents trick
   .topbar-logout-form already uses for the nav's own logout button, generalized under a reusable name
   since this is no longer only the nav's concern. */
.inline-form {
    display: contents;
}

/* Definition table - key/value rows (workout detail) */
.detail-table {
    width: 100%;
    border-collapse: collapse;
}

.detail-table th, .detail-table td {
    text-align: left;
    padding: 10px 4px;
    border-bottom: 1px solid var(--color-border);
    font-weight: 400;
}

.detail-table tr:last-child th, .detail-table tr:last-child td {
    border-bottom: none;
}

.detail-table th {
    width: 140px;
    color: var(--color-muted);
    font-size: 13px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

/* workout/show.html.twig - the details table used to sit full-width above the route map, making the
   map (a fixed 400px tall, full .page width) read as a small, secondary element beneath a wide,
   mostly-empty table. Narrowing the details into a sidebar and letting the map take the rest of the
   row - plus a taller map (see #workout-map's own height) - was raised directly ("table is too wide...
   map is too small"). Deliberately keeps .page's own 1240px width unchanged (a past request explicitly
   unified every non-map page onto one width - see CLAUDE.md's "Visual design system" - reopening that
   per-page isn't warranted just for this). */
.workout-layout {
    display: flex;
    align-items: flex-start;
    gap: 24px;
}

.workout-details {
    flex: 0 0 300px;
}

.workout-map-panel {
    flex: 1 1 auto;
    min-width: 0;
}

/* "Route" would otherwise inherit h2's global 32px margin-top, pushing it visibly lower than the
   details card sitting right beside it at the same starting height (reported directly from a real
   screenshot) - zeroed here since this is the first thing in its own column, not a section following
   other content the way h2 usually does elsewhere in this app. */
.workout-map-panel h2:first-child {
    margin-top: 0;
}

@media (max-width: 900px) {
    .workout-layout {
        flex-direction: column;
        /* align-items: flex-start (the row layout's cross-axis, i.e. vertical, alignment) becomes the
           *horizontal* cross-axis once flex-direction flips to column - left unreset, both children
           shrink-wrap to their own content width instead of filling the stacked column (caught by an
           actual screenshot at a narrow width, not assumed from the CSS alone - the map collapsed to
           the width of its own zoom-control buttons). stretch is column-mode's own correct default. */
        align-items: stretch;
    }

    .workout-details {
        flex-basis: auto;
        width: 100%;
    }
}

/* workout/show.html.twig's de-emphasized "Exclude from geo stats" toggle (see .link-button.link-muted
   above) - a little breathing room from whatever section sits above it, matching .osm-credit's own
   margin-top precedent for a trailing secondary element. */
.workout-footer-action {
    margin-top: 8px;
}

/* Data table (lists) */
.table-wrap {
    overflow-x: auto;
    background: var(--color-surface);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
}

table.data {
    width: 100%;
    border-collapse: collapse;
    font-size: 14px;
}

table.data th, table.data td {
    text-align: left;
    padding: 12px 16px;
    border-bottom: 1px solid var(--color-border);
}

table.data th {
    color: var(--color-muted);
    font-weight: 600;
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

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

table.data tbody tr:hover {
    background: var(--color-bg);
}

/* T20.6 (docs/TASKS.md Epic 20) - an action cell that sometimes holds a .btn (e.g. series/show's "View
   workouts") and sometimes nothing made its row noticeably taller than a row with no button, since
   table.data's row height is dictated by its tallest cell. Reserves the button's own rendered height
   whether or not a button is present. 41px, not the originally-estimated 37px - the first pass missed
   the 1.5 line-height on the button's 14px text (21px, not 14px), reported back as a real 4px
   discrepancy (66px vs 62px measured row heights) after the initial fix shipped. */
.row-action {
    display: flex;
    align-items: center;
    min-height: 41px;
}

/* boundary_overlaps.html.twig's inline status-change <select> sits inside a .row-action flex row next
   to its own submit button - the general `form select` rule above (width: 100%) would stretch it to
   fill the row and crowd the button out, so it's opted back out to its natural inline width here.
   The form itself becomes the flex row (not just .row-action, which only ever had the one <form> as
   its single flex item) with its own gap and wrap - without an explicit gap, the select and button
   sat right up against each other, and on a narrow column wrapped onto two lines with zero space
   between them (inline default wrapping has no vertical gap on its own). `gap` applies in both
   directions under flex-wrap, so this covers side-by-side and stacked alike. */
.row-action form {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px;
}

.row-action select {
    width: auto;
}

/* Status badges */
.badge {
    display: inline-block;
    padding: 3px 10px;
    border-radius: 999px;
    font-size: 12px;
    font-weight: 600;
    text-transform: capitalize;
    background: var(--color-pending-soft);
    color: var(--color-pending);
}

.badge-processed { background: var(--color-success-soft); color: var(--color-success); }
.badge-failed, .badge-invalid { background: var(--color-danger-soft); color: var(--color-danger); }
.badge-duplicate { background: #fff3bf; color: #966b00; }
.badge-pending { background: var(--color-pending-soft); color: var(--color-pending); }
/* T34.1 (docs/TASKS.md) - neutral, not danger/success: exclusion is a deliberate user choice, not
   an error state or an achievement, so it reuses .badge-pending's own neutral gray rather than
   getting a new color. */
.badge-excluded { background: var(--color-pending-soft); color: var(--color-pending); }

/* T36.3 (docs/TASKS.md Epic 36) - /series' "My series" list, requested directly: a completed series
   (every item visited) was otherwise indistinguishable from one still in progress once sorted
   alphabetically. Reuses .badge-processed's existing green/success treatment rather than a new color -
   "complete" is the same semantic as a successfully processed import row. */
.series-completed-heading { margin-top: 24px; font-size: 16px; color: var(--color-muted); }
.series-completed-row { background: var(--color-success-soft); }

/* Workout list modal - shared by the map page's cell/boundary click-through and the series item
   page's "view workouts" click-through (T20.7, docs/TASKS.md Epic 20). Originally only defined,
   scoped by id, in map/index.html.twig's own <style> block; pulled out here so a second page reusing
   the same modal concept doesn't end up with plain unstyled browser <dialog> chrome. */
.workout-modal {
    border: none;
    border-radius: var(--radius);
    padding: 20px 22px;
    box-shadow: var(--shadow-lg);
    font-family: var(--font-sans);
    min-width: 240px;
    max-width: 340px;
}

.workout-modal::backdrop { background: rgba(20, 20, 30, 0.35); }
.workout-modal h2 { margin: 0 0 12px; font-size: 16px; padding-right: 60px; }
.workout-modal ul { list-style: none; padding: 0; margin: 0; }
.workout-modal li { padding: 8px 0; border-bottom: 1px solid var(--color-border); font-size: 14px; }
.workout-modal li:last-child { border-bottom: none; }
.workout-modal .btn { position: absolute; top: 16px; right: 16px; padding: 5px 12px; font-size: 13px; }

/* Alerts / flash messages */
.alert {
    border-radius: var(--radius-sm);
    padding: 12px 16px;
    margin-bottom: 16px;
    font-size: 14px;
}

.alert-error {
    background: var(--color-danger-soft);
    color: var(--color-danger);
}

.alert-notice {
    background: var(--color-success-soft);
    color: var(--color-success);
}

/* Same amber as the reconnect banner, and for the same reason: an action is being asked of the user,
   not an error being reported to them (Epic 46, docs/TASKS.md). */
.alert-warning {
    background: var(--color-warning-soft);
    color: var(--color-warning);
}

/* block, not inline-block: an inline form sits *inside* the paragraph's text flow and gets wedged
   mid-sentence wherever the wrap happens to fall - caught in a browser, not by reading the CSS. */
.alert form { display: block; margin-top: 12px; }

/* Reconnect-your-account banner (Epic 45, docs/TASKS.md). Rendered by base.html.twig on every page
   rather than by any one controller, because the whole point is that it finds the user wherever they
   happen to be - there is no email channel to reach them by (User has no email address), so an
   in-app prompt is the only channel that exists. */
.reauth-banner {
    background: var(--color-warning-soft);
    color: var(--color-warning);
    border-bottom: 1px solid rgba(164, 98, 11, 0.2);
    font-size: 14px;
}

.reauth-banner-item {
    max-width: 1240px;
    margin: 0 auto;
    padding: 12px 20px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    flex-wrap: wrap;
}

.reauth-banner-item + .reauth-banner-item { border-top: 1px solid rgba(164, 98, 11, 0.2); }

/* Both are load-bearing, and the banner is visibly wrong without them: the text needs `min-width: 0`
   to be allowed to shrink below its own content width (a flex item's default `min-width: auto`
   refuses to), or it claims the whole row and wraps the button onto a second line - which on the map
   costs real canvas height for no reason. Verified in a browser at 1400 and 390 wide. */
.reauth-banner-text { flex: 1 1 280px; min-width: 0; }
.reauth-banner .btn { flex: none; }
.reauth-banner-unavailable { font-style: italic; opacity: 0.85; }

/* The map's variant. It floats over the canvas like everything else on that page (a document-flow
   strip would cost height from a 100vh canvas), anchored on *both* sides for the reason Epic 44
   established: a one-side-anchored auto-width box grows off-screen when its content outgrows the
   viewport. Positioned from --map-nav-bottom, the nav's real measured bottom edge, so it clears a
   nav that has wrapped to two rows on a narrow screen - never from a guessed constant. */
.reauth-banner--floating {
    position: absolute;
    top: calc(var(--map-nav-bottom, 80px) + 10px);
    left: 10px;
    right: 10px;
    z-index: 1100;
    border: 1px solid rgba(164, 98, 11, 0.25);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
}

.reauth-banner--floating .reauth-banner-item { padding: 10px 14px; }

@media (max-width: 640px) {
    .reauth-banner-item { padding: 10px 14px; gap: 10px; }
    .reauth-banner--floating .reauth-banner-item { font-size: 13px; }
}

/* Empty states */
.empty-state {
    background: var(--color-surface);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    padding: 40px 20px;
    text-align: center;
    color: var(--color-muted);
}

/* Series list (T35.2, docs/TASKS.md Epic 35) - "My series"/"Other series" split */
#my-series, #other-series {
    margin-bottom: 32px;
}

.series-search {
    max-width: 320px;
    margin-bottom: 16px;
}

.series-search input {
    margin-bottom: 0;
}

/* Screen-reader-only label for the search input above - no visible "Search" label/button is wanted
   (T35.2's own ask: in-place, no buttons), but the input still needs an accessible name. */
.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* Pagination */
.pager {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 4px;
    margin-top: 16px;
    font-size: 14px;
}

.pager a, .pager-current {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 32px;
    height: 32px;
    padding: 0 8px;
    border-radius: var(--radius-sm);
}

.pager a {
    color: var(--color-ink);
}

.pager a:hover {
    background: var(--color-bg);
    text-decoration: none;
}

.pager-current {
    background: var(--color-primary);
    color: #fff;
    font-weight: 600;
}

/* Upload form */
.upload-form input[type="file"] {
    display: block;
    width: 100%;
    padding: 10px;
    border: 1px dashed var(--color-border);
    border-radius: var(--radius-sm);
    background: var(--color-bg);
    margin: 8px 0 16px;
    font: inherit;
    font-size: 13px;
}

.upload-form label {
    font-weight: 600;
    font-size: 14px;
}

/* Login page */
.auth-shell {
    /* Was min-height: 100vh, which would now overflow by exactly the footer's height and put a
       scrollbar on the login page. `flex: 1 0 auto` fills whatever the footer leaves instead, and
       the card still centers in it - while a card taller than that space grows the shell rather than
       being crushed into the footer. */
    flex: 1 0 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
}

.auth-card {
    background: var(--color-surface);
    border-radius: var(--radius);
    box-shadow: var(--shadow-lg);
    padding: 40px 36px;
    max-width: 360px;
    width: 100%;
    text-align: center;
}

.auth-mark {
    width: 64px;
    height: 64px;
    margin: 0 auto 16px;
}

.auth-mark img {
    display: block;
    width: 100%;
    height: 100%;
    border-radius: 14px;
}

.auth-card h1 {
    font-size: 22px;
    margin-bottom: 4px;
}

.auth-card p.tagline {
    color: var(--color-muted);
    font-size: 14px;
    margin: 0 0 24px;
}

.auth-card .btn {
    width: 100%;
    justify-content: center;
    padding: 11px 16px;
}

/* Strava/Suunto/Garmin login buttons stack on the same login card (T21.1, docs/TASKS.md Epic 21) -
   without this they'd render flush against each other, since .btn itself carries no vertical margin. */
.auth-card .btn + .btn {
    margin-top: 10px;
}

.auth-card .meta {
    margin-top: 20px;
}


/* Geek mode (T60.3/T60.4, docs/TASKS.md Epic 60) - the FIT decoder's own output, on the workout page and
   on /workouts/{uuid}/fit. Reuses .workout-layout's two-column row rather than inventing a second one,
   with a wider left column: the header card's values are sentences ("157,096 of 157,096 declared bytes
   read"), not the short numbers the 300px details sidebar was sized for. */
.fit-layout .workout-details {
    flex: 0 0 420px;
}

/* A checkbox on one line with its own label. The global `label` rule is a block, bold, muted treatment
   written for a text field's caption, which reads as a heading rather than as an option when it sits
   beside a checkbox - the same targeted opt-out `.layer-control label` already makes for the map's
   layer toggles. */
.toggle-field {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 14px;
    font-weight: 400;
    color: var(--color-ink);
    margin-bottom: 14px;
}

.toggle-field input {
    margin: 0;
}

/* The message-type filter on the FIT page: one link per message type in the file, wrapping. A dozen
   types is the realistic count (the corpus census found 12), which is a row of links rather than a
   dropdown - every option, and its count, visible at once. */
.fit-filters {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-bottom: 16px;
}

.fit-filter {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 6px 12px;
    border: 1px solid var(--color-border);
    border-radius: 999px;
    font-size: 13px;
    text-decoration: none;
    color: var(--color-ink);
    background: var(--color-surface);
}

.fit-filter.is-active {
    border-color: var(--color-primary);
    background: var(--color-primary);
    color: #fff;
}

/* A mixed-message row lists its own fields inline - a column table across message types would be a
   hundred sparse columns. Each field is one non-breaking unit so a long row wraps between fields
   rather than inside one.

   **This must stay on a div inside the cell, never on the `<td>`.** `display: flex` replaces a cell's
   own `display: table-cell`, and a table row cannot hold a non-cell child: the browser generates an
   anonymous cell around it, so the box's border-bottom paints at its own height rather than the row's
   and its width stops being governed by its column. Both showed up in a real screenshot - row
   separators stepping out of line, and the fields column running off the right edge of the table. */
.fit-fields {
    display: flex;
    flex-wrap: wrap;
    gap: 4px 14px;
    font-size: 13px;
}

/* A `record` message carries a dozen-plus fields, so this column is the one that decides the table's
   width. Left to itself it sizes to max-content and pushes the table past its container (the
   .table-wrap then scrolls, which hides the left columns while reading the right ones). A percentage
   width lets it wrap into the space that's left instead - the other three columns are a counter, a byte
   offset and a message name, none of which need more than they take. */
table.fit-messages td:last-child {
    width: 65%;
}

.fit-field {
    white-space: nowrap;
}

.fit-field b {
    color: var(--color-muted);
    font-weight: 600;
}

/* Byte offsets and raw stored values: monospace, so a column of them lines up and can be compared with
   a hex dump, and muted because the decoded value beside it is the one being read. */
.fit-raw {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 12px;
    color: var(--color-muted);
}

/* The message list's own scroll box (Epic 67, docs/TASKS.md). A `record` table is a dozen columns wide
   and a hundred rows tall; left to grow, its only horizontal scrollbar sits at the very bottom of the
   table, so reading the right-hand columns meant scrolling the whole page down to reach it. Capping the
   box keeps both scrollbars on screen at once.

   The cap is viewport-relative rather than a fixed pixel height so a tall window shows more rows, and
   it is generous enough that a short page of messages does not scroll inside a box at all. */
.fit-table-scroll {
    max-height: calc(100vh - 220px);
    overflow: auto;
}

/* The header row stays put above the rows it names - a hundred rows of `record` with the column names
   scrolled off the top is a table nobody can read. It needs an opaque background of its own: a sticky
   cell paints over the rows passing under it, and `table.data th` sets no background, so the default
   transparent one would let them show through. */
.fit-table-scroll table.data thead th {
    position: sticky;
    top: 0;
    z-index: 1;
    background: var(--color-surface);
}

/* A filter or pager link lands on the paragraph that reports the counts, directly above the table. The
   margin is deliberately about one filter row tall: the tabs sit immediately above that paragraph, and
   landing with the tab you just clicked scrolled off the top would be its own version of the jump this
   anchor exists to stop. */
#fit-messages {
    scroll-margin-top: 92px;
}

/* The session panel's table reads down the page rather than across it - one row per field, one column
   per session. Its row headers are field names, so they take the muted treatment column headers have
   elsewhere, and a width, because without one the name column takes half the table when the values are
   short. `session` is the only message type that gets this; every other summary-shaped one reaches
   dozens or hundreds of messages in a real file (see `_fit_session.html.twig`). */
table.fit-session-table th[scope="row"] {
    width: 260px;
    color: var(--color-muted);
    font-weight: 600;
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.fit-session {
    margin-bottom: 24px;
}

/* A column or field the *file* defined rather than the FIT profile (Epic 68, docs/TASKS.md). Marked
   rather than separated: a developer field is a real measurement sitting in the same message as the
   native ones, and pulling it into its own table would break the row it belongs to - but a reader still
   has to be able to tell "the protocol defines this" from "this file made it up", not least because the
   two can legally share a name in one message. */
.fit-developer-mark {
    display: inline-block;
    padding: 1px 5px;
    border: 1px solid var(--color-border);
    border-radius: 4px;
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--color-muted);
    vertical-align: middle;
}

.fit-field.is-developer b,
table.fit-messages th.is-developer,
table.fit-session-table th.is-developer {
    color: var(--color-primary);
}

/* The developer-fields panel (Epic 66, docs/TASKS.md) - what a file declares about fields the Global
   Profile has never heard of. A `.card` like every other panel on the page; the only thing it needs of
   its own is breathing room around the one-line-per-application header above its table, since an
   application id is followed by its raw bytes on a second line and the two must read as one unit. */
.fit-developer {
    margin-bottom: 24px;
}

.fit-developer-app {
    margin: 0 0 12px;
    line-height: 1.6;
}

.fit-developer-app:last-of-type {
    margin-bottom: 18px;
}

/* Geek mode's heartbeat panel and its trace (Epic 63, docs/TASKS.md) - the `hrv` reconstruction drawn
   on /workouts/{uuid}/fit.

   **Every mark the chart draws is styled here, not in JavaScript.** An SVG element takes CSS like any
   other, so hrv_chart_controller.js sets geometry and class names only and the design system keeps its
   colours in one place - the opposite of map_controller.js's LOCATION_COLOR, which is duplicated by
   hand purely because Leaflet demands a literal colour string. */
.hrv-stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
    gap: 10px;
    margin: 16px 0;
}

.hrv-stat {
    background: var(--color-bg);
    border-radius: var(--radius-sm);
    padding: 10px 12px;
}

.hrv-stat-label,
.hrv-stat-note {
    display: block;
    font-size: 12px;
    color: var(--color-muted);
}

/* Proportional figures, deliberately: these sit in a grid of tiles, not in a column that has to line
   up digit for digit, and tabular ones read loose at this size. The axis ticks below do the opposite. */
.hrv-stat-value {
    display: block;
    font-size: 22px;
    font-weight: 600;
    line-height: 1.3;
}

.hrv-chart {
    position: relative;
    margin: 8px 0 4px;
}

.hrv-svg {
    display: block;
}

/* The band is the middle 90% of each slice's beats and the line is their median: the band's *width* is
   the variability the panel exists to show, which is why it is a wash under a thin line rather than a
   thicker line on its own. */
.hrv-band {
    fill: var(--color-primary);
    fill-opacity: 0.14;
}

/* The outright extremes, behind it. Faint enough that a lone stretched beat no longer decides what the
   whole chart looks like - which is what min-max alone did on a real degraded file - and present enough
   that nothing the device actually recorded goes undrawn. */
.hrv-band-extremes {
    fill: var(--color-primary);
    fill-opacity: 0.05;
}

.hrv-line {
    fill: none;
    stroke: var(--color-primary);
    stroke-width: 2;
    stroke-linejoin: round;
    stroke-linecap: round;
}

/* The device's own heart rate, expressed as the beat gap it implies so that it shares the beats' axis.
   Drawn on top of the median it normally coincides with - the coincidence is the point, since the two
   are independent answers to the same question. */
.hrv-heart {
    fill: none;
    stroke: var(--color-heart);
    stroke-width: 2;
    stroke-linejoin: round;
    stroke-linecap: round;
}

.hrv-gridline {
    stroke: var(--color-border);
    stroke-width: 1;
}

/* A stretch with the timer stopped. Neutral and very light - it explains a break in the trace, it is
   not itself a reading. */
.hrv-pause {
    fill: var(--color-muted);
    fill-opacity: 0.08;
}

/* Amber rather than red, the same distinction --color-warning was introduced for: a flagged beat is
   data quality worth seeing, not an error anybody made. */
.hrv-artefact {
    fill: var(--color-warning);
}

.hrv-crosshair {
    stroke: var(--color-muted);
    stroke-width: 1;
}

/* The 2px ring is in the surface colour, so the marker stays legible where it rides on top of the
   line it is reading. */
.hrv-marker {
    fill: var(--color-primary);
    stroke: var(--color-surface);
    stroke-width: 2;
}

.hrv-tick {
    font-size: 11px;
    fill: var(--color-muted);
    font-variant-numeric: tabular-nums;
}

.hrv-tick-y { text-anchor: end; }
.hrv-tick-x { text-anchor: middle; }
/* The left column's own values, said in bpm. One axis in two units - never a second scale. */
.hrv-tick-bpm { text-anchor: start; }

.hrv-tooltip {
    position: absolute;
    z-index: 2;
    pointer-events: none;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow);
    padding: 8px 10px;
    font-size: 12px;
    line-height: 1.45;
    white-space: nowrap;
}

.hrv-key {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px 18px;
    font-size: 12px;
    color: var(--color-muted);
    margin: 0 0 18px;
}

.hrv-key-item {
    display: inline-flex;
    align-items: center;
    gap: 6px;
}

.hrv-swatch {
    display: inline-block;
    width: 14px;
    height: 10px;
    border-radius: 2px;
}

.hrv-swatch-band { background: var(--color-primary); opacity: 0.2; }
.hrv-swatch-ghost { background: var(--color-primary); opacity: 0.08; }
.hrv-swatch-line { height: 2px; background: var(--color-primary); }
.hrv-swatch-artefact { height: 6px; background: var(--color-warning); }
.hrv-swatch-heart { height: 2px; background: var(--color-heart); }
.hrv-swatch-pause { background: var(--color-muted); opacity: 0.16; }

/* The rule the chart is judged by, in prose, under the key that names its marks. Runs the full width of
   the panel on purpose - a measure-limited column was tried and read as a narrow block stranded under a
   full-width chart. Only the bottom margin differs from `.meta`, to clear the table below it. */
.hrv-note {
    margin: 0 0 18px;
}

table.hrv-segments caption {
    text-align: left;
    padding-bottom: 8px;
}

@media (max-width: 900px) {
    .fit-layout .workout-details {
        flex-basis: auto;
        width: 100%;
    }
}

@media (max-width: 600px) {
    .page { padding: 12px 12px 40px; }
    .topbar { border-radius: var(--radius-sm); }
    table.data th, table.data td { padding: 10px; }
}
