@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap");

/* ============================================================
   Design tokens — shared with public site
   ============================================================ */
:root {
    --bg: #eef2ff;
    --paper: #f8fbff;
    --paper-strong: #ffffff;
    --ink: #0f172a;
    --muted: #475569;
    --line: #d9e2f2;
    --accent: #2563eb;
    --accent-strong: #1d4ed8;
    --accent-soft: #dbeafe;
    --signal: #1e40af;
    --signal-soft: #e0e7ff;
    --danger: #dc2626;
    --danger-soft: #fef2f2;
    --success: #16a34a;
    --success-soft: #f0fdf4;
    --warning: #d97706;
    --warning-soft: #fffbeb;
    --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.06);
    --shadow: 0 4px 16px rgba(37, 99, 235, 0.08);
    --shadow-lg: 0 12px 40px rgba(37, 99, 235, 0.12);
    --radius-sm: 6px;
    --radius-md: 10px;
    --radius-lg: 16px;
    --font-body: "Inter", "Noto Sans", "Helvetica Neue", sans-serif;
    --sidebar-w: 240px;
    --topbar-h: 56px;
}

@media (prefers-color-scheme: dark) {
    :root {
        color-scheme: dark;
        --bg: #0b1527;
        --paper: #0f1a2f;
        --paper-strong: #f8fafc;
        --ink: #e2e8f0;
        --muted: #94a3b8;
        --line: #334155;
        --accent: #60a5fa;
        --accent-strong: #3b82f6;
        --accent-soft: #1e3a5f;
        --signal: #93c5fd;
        --signal-soft: #1f3657;
        --danger: #f87171;
        --danger-soft: #1c1317;
        --success: #4ade80;
        --success-soft: #0f1d15;
        --warning: #fbbf24;
        --warning-soft: #1a1708;
        --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.2);
        --shadow: 0 4px 16px rgba(2, 6, 23, 0.3);
        --shadow-lg: 0 12px 40px rgba(2, 6, 23, 0.5);
    }
}

/* ============================================================
   Reset & base
   ============================================================ */
*, *::before, *::after { box-sizing: border-box; }
[hidden] { display: none !important; }
html { scroll-behavior: smooth; }
body {
    margin: 0;
    min-height: 100vh;
    font-family: var(--font-body);
    font-size: 14px;
    line-height: 1.5;
    color: var(--ink);
    background: var(--bg);
}
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

/* ============================================================
   App shell layout
   ============================================================ */
.app-shell {
    display: grid;
    grid-template-columns: var(--sidebar-w) 1fr;
    grid-template-rows: var(--topbar-h) 1fr;
    grid-template-areas:
        "sidebar topbar"
        "sidebar main";
    min-height: 100vh;
}

/* ---- Top bar ---- */
.app-topbar {
    grid-area: topbar;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 24px;
    background: var(--paper);
    border-bottom: 1px solid var(--line);
    position: sticky;
    top: 0;
    z-index: 20;
}

.app-topbar-left {
    display: flex;
    align-items: center;
    gap: 12px;
}

.app-topbar-left h1,
.app-topbar-left .app-topbar-title {
    font-size: 15px;
    font-weight: 600;
    margin: 0;
}

.app-breadcrumb {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: 13px;
    color: var(--muted);
}

.app-breadcrumb a { color: var(--muted); }
.app-breadcrumb a:hover { color: var(--accent); text-decoration: none; }
.app-breadcrumb .sep { opacity: 0.4; }

.app-topbar-right {
    display: flex;
    align-items: center;
    gap: 16px;
    font-size: 13px;
}

.app-user-name {
    color: var(--muted);
}

.app-topbar-right a {
    color: var(--muted);
    font-size: 13px;
}

/* ---- Sidebar ---- */
.app-sidebar {
    grid-area: sidebar;
    background: var(--paper);
    border-right: 1px solid var(--line);
    display: flex;
    flex-direction: column;
    position: sticky;
    top: 0;
    height: 100vh;
    overflow-y: auto;
    z-index: 30;
}

.app-sidebar-brand {
    display: flex;
    align-items: center;
    gap: 10px;
    height: var(--topbar-h);
    padding: 0 20px;
    border-bottom: 1px solid var(--line);
    font-weight: 700;
    font-size: 16px;
    color: var(--accent);
    text-decoration: none;
}
.app-sidebar-brand:hover { text-decoration: none; }

.app-sidebar-brand__mark {
    width: 26px;
    height: 26px;
    border-radius: var(--radius-sm);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-weight: 800;
    font-size: 14px;
    color: #fff;
    background: var(--accent);
    flex-shrink: 0;
}

.app-sidebar-tenant {
    padding: 12px 20px 8px;
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--ink);
}

.app-sidebar-nav {
    list-style: none;
    margin: 0;
    padding: 4px 8px;
    flex: 1;
    /* The product areas scroll INSIDE the sidebar rather than pushing the
       pinned block below the viewport. min-height:0 is what actually lets a
       flex child shrink enough to scroll. */
    overflow-y: auto;
    min-height: 0;
}

/* Collapsible product areas. `details` carries the keyboard and screen-reader
   behaviour; the script only remembers the state. */
.app-sidebar-group { list-style: none; }
.app-sidebar-group__body > summary {
    cursor: pointer;
    display: flex;
    align-items: center;
    gap: 6px;
    user-select: none;
}
.app-sidebar-group__body > summary::-webkit-details-marker { display: none; }
.app-sidebar-group__body > summary::marker { content: ''; }
.app-sidebar-group__body > summary::after {
    content: 'expand_more';
    font-family: 'Material Symbols Rounded', sans-serif;
    font-size: 16px;
    /* An icon glyph needs no leading. Inheriting the body's 1.5 gave this 16px
       chevron a 24px line box -- taller than the 17px label beside it, so the
       chevron, not the text or the padding, was setting the row height. */
    line-height: 1;
    margin-left: auto;
    opacity: .55;
    transition: transform .15s;
}
.app-sidebar-group__body[open] > summary::after { transform: rotate(180deg); }
.app-sidebar-group__body > summary:hover { color: var(--ink); }
.app-sidebar-sublist { list-style: none; margin: 0; padding: 0; }

/* Nothing is pinned. Anchoring Ylläpito stopped the scrolling but reserved
   190px of a 768px screen for four occasional links, which is room the daily
   work needed. With one section open at a time the list fits on its own. */

.app-sidebar-nav li a {
    display: flex;
    align-items: center;
    gap: 10px;
    /* Denser than the original 8px. A pointer does not need a 40px row, and the
       tighter rhythm is what keeps the whole list on screen. The mobile drawer
       restores the roomier padding below, where the target is a fingertip. */
    padding: 5px 12px;
    border-radius: var(--radius-sm);
    font-size: 13px;
    font-weight: 500;
    color: var(--ink);
    text-decoration: none;
    transition: background 0.15s;
}

.app-sidebar-nav li a:hover {
    background: var(--accent-soft);
    text-decoration: none;
}

.app-sidebar-nav li a.is-active {
    background: var(--accent-soft);
    color: var(--accent);
    font-weight: 600;
}

.app-sidebar-nav .nav-icon {
    font-size: 18px;
    /* Same reason as the chevron: an 18px glyph was claiming a 27px line box
       and setting the height of every link row. */
    line-height: 1;
    width: 20px;
    text-align: center;
    font-variation-settings: 'FILL' 0, 'wght' 400;
}

.app-sidebar-section {
    /* Tightened: the 16px lead-in was sized for a handful of headings, and with
       every group now carrying one it added up. Left padding matches the links
       below it so headings and items share an edge. */
    padding: 6px 12px 2px;
    font-size: 11px;
    line-height: 1.3;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--muted);
}

.app-sidebar-footer {
    padding: 12px 20px;
    border-top: 1px solid var(--line);
    font-size: 12px;
    color: var(--muted);
}

/* YKSI NAVIGAATIO (18.8.2026): a page that brings its own complete menu
   (app/layout.php: $page_brings_own_nav) gets a slim return/identity strip
   instead of the full platform sidebar, so the first viewport holds one
   navigation. The strip keeps the sidebar's own building blocks — brand,
   tenant, footer — and adds only the named way back. */
.app-shell--own-nav {
    grid-template-columns: 216px 1fr;
}

.app-sidebar--strip .app-sidebar-tenant {
    white-space: normal;
}

/* KIRJANPIDON TYÖTILA (Kirjanpito UI slice A, 28.9.2026): the rail carries the
   workspace's destinations, so it is the platform sidebar's own list styles
   (.app-sidebar-nav, .app-sidebar-section) with two additions — the working
   context under the company, and the deliberate way out to Fleksi. */
.app-sidebar-context {
    padding: 0 20px 8px;
    font-size: 12px;
    color: var(--muted);
}

.app-sidebar-workspace-nav {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
}

.app-sidebar-workspace-nav .app-sidebar-nav {
    overflow-y: visible;
}

.app-sidebar-exit {
    margin-top: 0;
}

.app-sidebar-return {
    display: flex;
    align-items: center;
    gap: 6px;
    margin: 4px 16px 12px;
    font-size: 12px;
    color: var(--muted);
    text-decoration: none;
}

.app-sidebar-return:hover {
    color: var(--accent);
}

.app-sidebar--strip .app-sidebar-footer {
    margin-top: auto;
}

/* ---- Main content ---- */
.app-main {
    grid-area: main;
    padding: 24px 32px;
    overflow-y: auto;
    /* Grid items default to min-width:auto, which lets wide content (tables,
       long strings) push the column past the viewport. Pin to 0 so the main
       column never exceeds the screen — wide content scrolls inside its own
       container instead of overflowing the whole page on mobile. */
    min-width: 0;
}

.app-page-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 24px;
}

.app-page-header h1 {
    font-size: 22px;
    font-weight: 700;
    margin: 0;
}

/* ============================================================
   Cards & panels
   ============================================================ */
.app-card {
    background: var(--paper);
    /* a <button class="app-card"> must follow the theme text colour, not the
       UA-default button colour (which is dim on dark backgrounds) — matches
       .app-action-card. */
    color: inherit;
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
    padding: 20px;
    box-shadow: var(--shadow-sm);
}

.app-card-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 16px;
}

.app-card-header h2 {
    font-size: 15px;
    font-weight: 600;
    margin: 0;
}

.app-card + .app-card {
    margin-top: 16px;
}

/* ============================================================
   Action dashboard — operational cockpit
   ============================================================ */
.app-action-strip {
    display: grid;
    grid-template-columns: repeat(6, minmax(0, 1fr));
    gap: 12px;
    margin-bottom: 20px;
}

.app-action-card {
    display: block;
    background: var(--paper);
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
    padding: 16px 20px;
    text-decoration: none;
    color: inherit;
    box-shadow: var(--shadow-sm);
    transition: box-shadow 0.15s;
}

.app-action-card:hover {
    box-shadow: var(--shadow-md, 0 4px 12px rgba(0,0,0,0.10));
    border-color: var(--accent);
}

.app-action-card--warn {
    border-color: var(--warning, #d97706);
}

.app-action-card--bill {
    border-color: var(--success, #2a7a2a);
}

.app-action-card-value {
    font-size: 32px;
    font-weight: 700;
    line-height: 1.1;
    margin-bottom: 4px;
}

.app-action-card-label {
    font-size: 12px;
    font-weight: 500;
    color: var(--muted);
    text-transform: uppercase;
    letter-spacing: 0.03em;
}

/* Worker job cards */
.app-job-cards {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.app-job-card {
    display: block;
    padding: 12px 14px;
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
    text-decoration: none;
    color: inherit;
    transition: background 0.1s, border-color 0.1s;
}

@media (hover: hover) {
    .app-job-card:hover {
        background: var(--accent-soft);
        border-color: var(--accent);
    }
}

.app-job-card-title {
    font-weight: 600;
    font-size: 14px;
    margin-bottom: 6px;
}

.app-job-card-meta {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    font-size: 12px;
}

/* ============================================================
   Shift week coverage grid (records-shift-week.php)
   ============================================================ */

.app-shift-week-nav {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 16px;
    gap: 12px;
}

.app-shift-week-nav-label {
    font-weight: 600;
    font-size: 14px;
    text-align: center;
}

.app-shift-week-grid {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    gap: 8px;
    overflow-x: auto;
    margin-bottom: 16px;
}

.app-shift-week-day {
    min-width: 110px;
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
    background: var(--paper);
    overflow: hidden;
}

.app-shift-week-day--today {
    border-color: var(--accent);
}

.app-shift-week-day--empty {
    opacity: 0.55;
}

.app-shift-week-day-header {
    padding: 8px 10px 6px;
    border-bottom: 1px solid var(--line);
    background: var(--canvas, var(--bg));
}

.app-shift-week-day-abbr {
    display: block;
    font-size: 10px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--muted);
}

.app-shift-week-day-num {
    display: block;
    font-size: 22px;
    font-weight: 700;
    line-height: 1.2;
}

.app-shift-week-day--today .app-shift-week-day-num {
    color: var(--accent);
}

.app-shift-week-day-coverage {
    display: flex;
    gap: 6px;
    margin-top: 4px;
    flex-wrap: wrap;
}

.app-shift-week-cov {
    font-size: 11px;
    font-weight: 600;
    padding: 1px 5px;
    border-radius: 9999px;
}

.app-shift-week-cov--ok {
    background: var(--success-soft);
    color: var(--success);
}

.app-shift-week-cov--warn {
    background: var(--warning-soft);
    color: var(--warning);
}

.app-shift-week-cov--danger {
    background: var(--danger-soft);
    color: var(--danger);
}

.app-shift-week-day-shifts {
    padding: 6px;
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-height: 36px;
}

.app-shift-week-shift {
    display: block;
    padding: 5px 8px;
    border-radius: var(--radius-sm);
    border: 1px solid var(--line);
    border-left-width: 3px;
    text-decoration: none;
    color: inherit;
    font-size: 12px;
    background: var(--paper);
    transition: background 0.1s;
}

@media (hover: hover) {
    .app-shift-week-shift:hover {
        background: var(--accent-soft);
    }
}

.app-shift-week-shift--confirmed {
    border-left-color: var(--success);
}

.app-shift-week-shift--unconfirmed {
    border-left-color: var(--warning);
}

.app-shift-week-shift--unassigned {
    border-left-color: var(--danger);
    background: var(--danger-soft);
}

.app-shift-week-shift--unassigned .app-shift-week-shift-person {
    color: var(--danger);
    font-style: italic;
}

.app-shift-week-shift--cancelled {
    border-left-color: var(--muted);
    opacity: .45;
}

.app-shift-week-shift--cancelled .app-shift-week-shift-person {
    text-decoration: line-through;
}

.app-shift-week-shift-person {
    font-weight: 600;
    margin-bottom: 2px;
}

.app-shift-week-shift-time,
.app-shift-week-shift-role {
    color: var(--muted);
    font-size: 11px;
}

.app-shift-week-confirm-btn {
    display: block;
    width: 100%;
    padding: 3px 6px;
    font-size: 11px;
    font-weight: 600;
    color: var(--accent);
    background: var(--accent-soft);
    border: 1px solid var(--accent);
    border-radius: var(--radius-sm);
    cursor: pointer;
    text-align: center;
    white-space: nowrap;
}

@media (hover: hover) {
    .app-shift-week-confirm-btn:hover {
        background: var(--accent);
        color: #fff;
    }
}

.app-shift-week-gap {
    font-size: 12px;
    color: var(--muted);
    padding: 6px 4px;
    font-style: italic;
}

.app-shift-week-summary {
    display: flex;
    gap: 16px;
    flex-wrap: wrap;
    font-size: 13px;
    padding: 10px 0;
    border-top: 1px solid var(--line);
    margin-top: 4px;
}

.app-shift-week-sum-ok   { color: var(--success); font-weight: 500; }
.app-shift-week-sum-warn { color: var(--warning);  font-weight: 500; }
.app-shift-week-sum-gap  { color: var(--danger);   font-weight: 500; }

/* On mobile: hide week grid, keep card list */
@media (max-width: 640px) {
    .app-shift-week-grid { display: none; }
    .app-shift-week-nav  { display: none; }
    .app-shift-week-summary { display: none; }
}

.app-shift-week-flash {
    font-weight: 500;
    font-size: 14px;
    padding: 14px 18px;
    margin-bottom: 14px;
    border-radius: var(--radius-sm);
    box-shadow: 0 2px 8px rgba(0,0,0,.08);
}

/* Shift brief (shift-detail.php header card) */
.app-shift-brief-card {
    margin-bottom: 16px;
}

.app-shift-brief {
    display: flex;
    flex-wrap: wrap;
    gap: 10px 24px;
}

.app-shift-brief-item {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: 14px;
}

.app-shift-brief-item .material-symbols-outlined {
    font-size: 18px;
    color: var(--muted);
}

.app-shift-brief-tpl {
    margin-top: 10px;
    padding-top: 10px;
    border-top: 1px solid var(--line);
    font-size: 12px;
    color: var(--muted);
}

/* Lead activity timeline */
.app-lead-timeline {
    display: flex;
    flex-direction: column;
}

.app-lead-timeline-item {
    display: flex;
    gap: 10px;
    padding: 10px 0;
    border-bottom: 1px solid var(--line);
}

.app-lead-timeline-item:last-child {
    border-bottom: none;
}

.app-lead-timeline-icon {
    flex-shrink: 0;
    width: 28px;
    height: 28px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    background: var(--canvas);
    color: var(--muted);
}

.app-lead-timeline-item--transition .app-lead-timeline-icon {
    background: var(--accent-soft);
    color: var(--accent);
}

.app-lead-timeline-body {
    flex: 1;
    min-width: 0;
}

.app-lead-timeline-item--auto {
    opacity: .55;
}

.app-lead-timeline-item--auto .app-lead-timeline-icon {
    width: 22px;
    height: 22px;
}

/* ============================================================
   Stats grid (dashboard)
   ============================================================ */
.app-stats {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
    gap: 16px;
    margin-bottom: 24px;
}

.app-stat {
    background: var(--paper);
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
    padding: 16px 20px;
    box-shadow: var(--shadow-sm);
}

.app-stat-label {
    font-size: 12px;
    font-weight: 500;
    color: var(--muted);
    text-transform: uppercase;
    letter-spacing: 0.03em;
    margin-bottom: 4px;
}

.app-stat-value {
    font-size: 28px;
    font-weight: 700;
    line-height: 1.2;
}

/* ============================================================
   Tables
   ============================================================ */
.app-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 13px;
}

.app-table th {
    text-align: left;
    padding: 10px 12px;
    font-weight: 600;
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--muted);
    border-bottom: 2px solid var(--line);
}

.app-table td {
    padding: 10px 12px;
    border-bottom: 1px solid var(--line);
    vertical-align: middle;
}

.app-table tbody tr:hover {
    background: var(--accent-soft);
}

.app-table-link {
    color: var(--ink);
    font-weight: 500;
}

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

/* ============================================================
   Badges / status pills
   ============================================================ */
.app-badge {
    display: inline-block;
    padding: 2px 10px;
    border-radius: 999px;
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.03em;
}

.app-badge--draft     { background: var(--line); color: var(--muted); }
.app-badge--active    { background: var(--accent-soft); color: var(--accent); }
.app-badge--progress  { background: var(--signal-soft); color: var(--signal); }
.app-badge--done      { background: var(--success-soft); color: var(--success); }
.app-badge--danger    { background: var(--danger-soft); color: var(--danger); }
.app-badge--warning   { background: var(--warning-soft); color: var(--warning); }

/* Invoice state pills on billing card */
.app-badge--success {
    background: var(--success-soft);
    color: var(--success);
    border-color: var(--success);
}
.app-badge--signal {
    background: var(--signal-soft);
    color: var(--signal);
    border-color: var(--signal);
}
.app-badge--muted {
    background: transparent;
    color: var(--muted);
    border-color: var(--line);
}

/* Job status summary row — manager at-a-glance on work order detail */
.app-job-status-row {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    margin-bottom: 20px;
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
    background: var(--paper);
    overflow: hidden;
}
.app-job-status-item {
    padding: 10px 14px;
    border-right: 1px solid var(--line);
}
.app-job-status-item:last-child { border-right: none; }
.app-job-status-label {
    display: block;
    font-size: 10px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--muted);
    margin-bottom: 3px;
}
.app-job-status-value {
    font-size: 13px;
    font-weight: 500;
    color: var(--ink);
}
.app-job-status-missing { color: var(--warning); }

/* Worker job card next-action hint */
.app-job-card-secondary {
    font-size: 11px;
    color: var(--muted);
    margin-top: 5px;
}

.app-job-card-hint {
    margin-top: 6px;
    font-size: 12px;
    color: var(--accent);
    font-weight: 500;
}

/* Worker action bar on work order detail (mobile-first field actions) */
.app-worker-bar {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    margin-bottom: 16px;
    padding: 12px 14px;
    background: var(--accent-soft);
    border-radius: var(--radius-md);
}
.app-btn--worker-action {
    font-size: 15px;
    padding: 10px 20px;
    min-width: 140px;
    text-align: center;
}
.app-worker-location {
    font-size: 13px;
    color: var(--ink);
}

/* Invoice lifecycle step track */
.app-inv-track {
    display: flex;
    align-items: flex-start;
    gap: 0;
    margin: 12px 0 4px;
}
.app-inv-track-step {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    position: relative;
    font-size: 11px;
    color: var(--muted);
}
.app-inv-track-step::before {
    content: '';
    position: absolute;
    top: 9px;
    left: calc(50% + 9px);
    right: calc(-50% + 9px);
    height: 2px;
    background: var(--line);
}
.app-inv-track-step:last-child::before { display: none; }
.app-inv-track-dot {
    width: 18px;
    height: 18px;
    border-radius: 50%;
    border: 2px solid var(--line);
    background: var(--paper);
    margin-bottom: 5px;
    position: relative;
    z-index: 1;
}
.app-inv-track-step--done .app-inv-track-dot {
    border-color: var(--accent);
    background: var(--accent);
}
.app-inv-track-step--done::before { background: var(--accent); }
.app-inv-track-step--done { color: var(--accent); }
.app-inv-track-step--current .app-inv-track-dot {
    border-color: var(--accent);
    background: var(--paper);
    box-shadow: 0 0 0 3px var(--accent-soft);
}
.app-inv-track-step--current { color: var(--ink); font-weight: 600; }
.app-inv-track-label { text-align: center; line-height: 1.3; }

/* Worker job brief description */
.app-worker-desc {
    font-size: 14px;
    line-height: 1.5;
    margin: 0 0 12px;
    color: var(--ink);
}

/* ============================================================
   Buttons
   ============================================================ */
.app-btn {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 8px 16px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--paper);
    color: var(--ink);
    font-size: 13px;
    font-weight: 500;
    font-family: inherit;
    cursor: pointer;
    transition: background 0.15s, border-color 0.15s;
    text-decoration: none;
}

.app-btn:hover {
    background: var(--accent-soft);
    border-color: var(--accent);
    text-decoration: none;
}

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

.app-btn--primary:hover {
    background: var(--accent-strong);
    border-color: var(--accent-strong);
}

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

.app-btn--danger:hover {
    background: var(--danger-soft);
}

.app-btn--sm {
    padding: 4px 10px;
    font-size: 12px;
}

/* ============================================================
   Forms
   ============================================================ */
.app-form-group {
    margin-bottom: 16px;
}

.app-form-group label {
    display: block;
    font-size: 12px;
    font-weight: 600;
    color: var(--muted);
    margin-bottom: 4px;
    text-transform: uppercase;
    letter-spacing: 0.03em;
}

.app-input,
.app-select,
.app-textarea {
    width: 100%;
    padding: 8px 12px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--paper);
    color: var(--ink);
    font-size: 14px;
    font-family: inherit;
    transition: border-color 0.15s;
}

.app-input:focus,
.app-select:focus,
.app-textarea:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-soft);
}

.app-textarea { resize: vertical; min-height: 80px; }

/* ------------------------------------------------------------
   Phone: no text-entry control below 16px
   ------------------------------------------------------------
   iOS Safari zooms the whole viewport when a field whose font is
   under 16px takes focus, and does not zoom back out. On a
   bookkeeping form that is one forced zoom PER FIELD — the memo
   voucher alone asks for 48 of them.

   Measured at 375px on 2026-09-03, before this rule: across
   etusivu, tiliöintiehdotukset, pankki, tositteet, muistiotosite,
   kuukauden päätös, ALV and jono, 129 of 129 visible fields were
   under 16px. Not most — all of them.

   Three separate causes, which is why one element-level rule with
   !important is the only thing that closes it:
     1. `.app-input`/`.app-select`/`.app-textarea` declare 14px, and
        a bare element selector loses to a class;
     2. the statement-import form's fields carry NO class and fall
        to the 13.33px UA default, which no `.app-input` rule reaches;
     3. the statement paste box sets `font-size:12px` INLINE, and an
        inline declaration outranks every media query without it.
   Same reason `.mv-lines td .app-input` below needs `width: 100%
   !important` — an inline desktop width must not survive on a phone.

   Scope is deliberate: text-entry controls only. Checkboxes, radios,
   buttons and ranges are excluded because iOS does not focus-zoom
   them and their box scales with the font. Pinned by
   tests/accounting_mobile_input_zoom_test.php. */
@media (max-width: 640px) {
    input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]):not([type="button"]):not([type="submit"]):not([type="reset"]):not([type="image"]):not([type="file"]),
    select,
    textarea {
        font-size: 16px !important;
    }
}

/* A placeholder that states a real value — "oletus 14" for a payment term the
   company will actually apply — is information, not an absence, so it is not
   dimmed the way a faint "type here" prompt is. Browsers grey placeholders by
   default and Firefox also drops their opacity, so both are reset. Only fields
   that opted in via veksi_field_input() are affected; ordinary placeholders
   keep the usual quiet styling. */
.app-input--hinted::placeholder {
    color: var(--ink);
    opacity: 1;
}

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

.app-alert--success { background: var(--success-soft); color: var(--success); }
.app-alert--danger  { background: var(--danger-soft); color: var(--danger); }
.app-alert--warning { background: var(--warning-soft); color: var(--warning); }
.app-alert--info    { background: var(--accent-soft); color: var(--accent); }

.invoice-payment-readiness {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 12px;
    border-left: 3px solid var(--warning);
}

.invoice-payment-readiness__body {
    flex: 1 1 260px;
    min-width: 0;
}

.invoice-payment-readiness__body p {
    margin: 6px 0 0;
}

.invoice-payment-readiness__meta {
    display: flex;
    flex-wrap: wrap;
    gap: 6px 12px;
    margin-top: 8px;
    color: var(--muted);
    font-size: 12px;
}

.invoice-payment-readiness__next {
    font-weight: 600;
}

.invoice-payment-readiness__actions {
    display: flex;
    flex: 0 1 220px;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 8px;
    min-width: 0;
}

.invoice-payment-readiness__actions .app-btn {
    max-width: 100%;
    white-space: normal;
}

@media (max-width: 640px) {
    .invoice-payment-readiness {
        flex-direction: column;
        justify-content: flex-start;
    }

    .invoice-payment-readiness__body {
        flex: 0 1 auto;
        width: 100%;
    }

    .invoice-payment-readiness__actions {
        flex: 1 1 auto;
        justify-content: stretch;
        width: 100%;
    }

    .invoice-payment-readiness__actions .app-btn {
        width: 100%;
        justify-content: center;
    }
}

/* ============================================================
   Review strip (time entry / expense report manager summary)
   ============================================================ */
.app-review-strip {
    display: flex;
    gap: 12px;
    flex-wrap: wrap;
    margin-bottom: 16px;
}

.app-review-strip-item {
    display: flex;
    flex-direction: column;
    padding: 10px 16px;
    border-radius: var(--radius-sm);
    border: 1px solid var(--line);
    background: var(--paper);
    text-decoration: none;
    color: var(--ink);
    min-width: 100px;
}

@media (hover: hover) {
    .app-review-strip-item:hover { border-color: var(--accent); }
}

.app-review-strip-val {
    font-size: 18px;
    font-weight: 700;
    line-height: 1.2;
}

.app-review-strip-label {
    font-size: 12px;
    color: var(--muted);
    margin-top: 2px;
}

.app-review-strip-item--warn {
    border-left: 3px solid var(--warning);
}

.app-review-strip-item--warn .app-review-strip-val {
    color: var(--warning);
}

.app-review-strip-item--ok {
    border-left: 3px solid var(--success);
}

.app-review-strip-item--ok .app-review-strip-val {
    color: var(--success);
}

.app-review-strip-item--accent {
    border-left: 3px solid var(--accent);
}

.app-review-strip-item--accent .app-review-strip-val {
    color: var(--accent);
}

/* ============================================================
   Self-queue: worker's own records grouped by what to do next.
   Card list is visible on ALL viewports (this is the primary
   worker surface, not a mobile-only fallback).
   ============================================================ */
.app-queue-summary {
    display: flex;
    align-items: baseline;
    gap: 8px;
    margin-bottom: 20px;
}
.app-queue-summary__val {
    font-size: 28px;
    font-weight: 700;
    color: var(--ink);
    line-height: 1;
}
.app-queue-summary__label {
    font-size: 13px;
    color: var(--muted);
}

.app-queue-group { margin-bottom: 24px; }

.app-queue-group__head {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 14px;
    font-weight: 600;
    color: var(--ink);
    margin: 0 0 10px;
    padding-left: 10px;
    border-left: 3px solid var(--line);
}
.app-queue-group__head--danger { border-left-color: var(--danger); color: var(--danger); }
.app-queue-group__head--warn   { border-left-color: var(--warning); }
.app-queue-group__head--ok      { border-left-color: var(--success); }

.app-queue-group__count {
    margin-left: auto;
    font-size: 12px;
    font-weight: 600;
    color: var(--muted);
    background: var(--paper);
    border: 1px solid var(--line);
    border-radius: 999px;
    padding: 1px 9px;
}

.app-queue-cards {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

/* Manager approval queue */
.app-approve-bar {
    display: flex;
    align-items: center;
    gap: 16px;
    margin-bottom: 12px;
}
.app-approve-bar__sel {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: 13px;
    color: var(--muted);
    cursor: pointer;
}
.app-approve-owner td { background: var(--paper); }

.app-flag {
    display: inline-block;
    font-size: 11px;
    font-weight: 600;
    line-height: 1.5;
    padding: 1px 8px;
    border-radius: 999px;
    margin: 1px 2px 1px 0;
    white-space: nowrap;
}
.app-flag--warn {
    color: var(--danger);
    background: var(--danger-soft);
    border: 1px solid var(--danger);
}
.app-flag--info {
    color: var(--muted);
    background: var(--paper);
    border: 1px solid var(--line);
}

/* ============================================================
   Public demo — banner + one-time splash. Mobile-first: ~80% of
   demo visitors are on phones. Full-width splash card, >=44px taps.
   ============================================================ */
.app-demo-banner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    flex-wrap: wrap;
    margin: 0 0 16px;
    padding: 10px 14px;
    border-radius: var(--radius-sm);
    background: var(--accent-soft);
    color: var(--accent-strong);
    font-size: 13px;
    font-weight: 600;
}
.app-demo-banner__actions { display: flex; gap: 8px; flex-wrap: wrap; }
.app-demo-banner .app-btn { min-height: 40px; }

.app-demo-splash {
    position: fixed;
    inset: 0;
    z-index: 1000;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 16px;
    overflow: auto;
    -webkit-overflow-scrolling: touch;
    background: rgba(15, 23, 42, 0.55);
}
.app-demo-splash__card {
    background: var(--paper-strong, #fff);
    border-radius: var(--radius-lg);
    padding: 24px;
    width: 100%;
    max-width: 480px;
    box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25);
}
.app-demo-splash__card h2 { margin: 0 0 10px; font-size: 20px; }
.app-demo-splash__card p { margin: 0 0 18px; font-size: 15px; line-height: 1.5; color: var(--muted); }
.app-demo-splash__actions { display: flex; gap: 10px; flex-wrap: wrap; }
.app-demo-splash__actions .app-btn { min-height: 44px; flex: 1 1 140px; justify-content: center; }

@media (max-width: 640px) {
    .app-demo-splash__card { padding: 18px; }
    .app-demo-banner { font-size: 12px; }
}

/* ============================================================
   Empty state
   ============================================================ */
.app-empty {
    text-align: center;
    padding: 48px 24px;
    color: var(--muted);
}

.app-empty-icon {
    font-size: 48px;
    margin-bottom: 12px;
    opacity: 0.4;
}

.app-empty h3 {
    font-size: 16px;
    font-weight: 600;
    margin: 0 0 8px;
    color: var(--ink);
}

.app-empty p {
    font-size: 13px;
    margin: 0;
}

/* ============================================================
   Detail page
   ============================================================ */
.app-detail-grid {
    display: grid;
    grid-template-columns: 1fr 300px;
    gap: 24px;
    align-items: start;
}

.app-detail-fields {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px 24px;
}

.app-detail-field label {
    display: block;
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--muted);
    margin-bottom: 2px;
}

.app-detail-field .value {
    font-size: 14px;
}

/* Workflow actions sidebar */
.app-workflow-actions {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

/* On mobile: workflow transition buttons become large primary CTAs */
@media (max-width: 640px) {
    .app-workflow-actions .app-btn {
        background: var(--accent);
        color: white;
        border-color: var(--accent);
        font-size: 16px;
        padding: 13px 16px;
        font-weight: 600;
        justify-content: center;
    }
    .app-workflow-actions .app-btn:hover {
        opacity: 0.88;
        background: var(--accent);
    }
}

/* ============================================================
   Mobile menu toggle
   ============================================================ */
.app-menu-toggle {
    display: none;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    border: none;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--ink);
    cursor: pointer;
    font-size: 22px;
    padding: 0;
}

.app-menu-toggle:hover {
    background: var(--accent-soft);
}

.app-sidebar-overlay {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.4);
    z-index: 29;
}

/* ============================================================
   Responsive
   ============================================================ */
@media (max-width: 900px) {
    /* The drawer is driven by a fingertip, not a pointer: keep the roomier
       rows the desktop sidebar gives up for density. */
    .app-sidebar-nav li a { padding: 9px 12px; }
    .app-sidebar-section  { padding: 12px 12px 4px; }

    /* Action strip: 3 columns on tablet */
    .app-action-strip {
        grid-template-columns: repeat(3, minmax(0, 1fr));
    }

    .app-shell {
        grid-template-columns: 1fr;
        grid-template-areas:
            "topbar"
            "main";
    }

    .app-menu-toggle {
        display: flex;
    }

    /* A CLOSED DRAWER IS NOT IN THE TAB ORDER (28.9.2026). Off-canvas is
       only a transform: every link in a closed drawer stayed focusable, so a
       keyboard reader tabbed through an invisible menu before reaching the
       page. visibility follows the transform, delayed on close so the slide
       still plays. */
    .app-sidebar {
        position: fixed;
        top: 0;
        left: 0;
        bottom: 0;
        width: 280px;
        transform: translateX(-100%);
        visibility: hidden;
        transition: transform 0.25s ease, visibility 0s linear 0.25s;
        z-index: 30;
    }

    .app-sidebar.is-open {
        transform: translateX(0);
        visibility: visible;
        transition: transform 0.25s ease, visibility 0s;
    }

    .app-sidebar-overlay.is-open {
        display: block;
    }

    .app-topbar {
        padding: 0 16px;
    }

    .app-topbar-right .app-user-name {
        display: none;
    }

    .app-main {
        padding: 16px;
    }

    .app-page-header {
        flex-direction: column;
        align-items: flex-start;
        gap: 12px;
    }

    .app-page-header h1 {
        font-size: 18px;
    }

    /* Stats grid */
    .app-stats {
        grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
        gap: 10px;
    }

    .app-stat {
        padding: 12px 14px;
    }

    .app-stat-value {
        font-size: 22px;
    }

    /* Tables */
    .app-table th,
    .app-table td {
        padding: 8px 10px;
        font-size: 12px;
    }

    /* Detail page */
    .app-detail-grid {
        grid-template-columns: minmax(0, 1fr);
    }

    /* Grid items default to min-width:auto, which lets a non-wrapping
       nested flex row (card-header actions) push the single mobile
       column wider than the viewport. Pin both column children to 0 so
       their content scrolls/wraps internally instead. */
    .app-detail-grid > div {
        min-width: 0;
    }

    /* The record itself comes first on mobile; status and history follow.
       (Kimmo 11.8.2026 — the earlier order:-1 that surfaced the workflow
       sidebar above the detail cards read as the page being upside down.) */

    /* Card header action rows (Tyoraportti / Laheta asiakkaalle / Muokkaa,
       etc.) must wrap instead of forcing the card - and with it the whole
       detail-grid column - wider than the viewport. */
    .app-card-header {
        flex-wrap: wrap;
        row-gap: 8px;
    }

    .app-card-header > div {
        flex-wrap: wrap;
        min-width: 0;
    }

    .app-detail-fields {
        grid-template-columns: 1fr;
    }

    /* Cards */
    .app-card {
        padding: 16px;
    }

    /* Breadcrumb */
    .app-breadcrumb {
        font-size: 12px;
        flex-wrap: wrap;
    }
}

@media (max-width: 480px) {
    .app-stats {
        grid-template-columns: 1fr 1fr;
    }

    /* Action strip: single column on mobile */
    .app-action-strip {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }

    .app-action-card-value {
        font-size: 24px;
    }

    /* Records table: horizontal scroll instead of compression */
    .app-card {
        overflow-x: auto;
    }

    .app-topbar-left h1,
    .app-topbar-left .app-topbar-title {
        font-size: 14px;
    }
}

/* ============================================================
   Line item edit tables — mobile card layout
   ============================================================ */
@media (max-width: 640px) {
    .line-items-edit-table thead {
        display: none;
    }

    .line-items-edit-table,
    .line-items-edit-table tbody,
    .line-items-edit-table tr,
    .line-items-edit-table td {
        display: block;
        width: 100%;
    }

    .line-items-edit-table tr {
        border: 1px solid var(--line);
        border-radius: var(--radius-sm);
        padding: 10px;
        margin-bottom: 10px;
        position: relative;
    }

    .line-items-edit-table td {
        padding: 4px 0 !important;
        text-align: left !important;
        width: 100% !important;
    }

    /* Field labels via data-label */
    .line-items-edit-table td[data-label]:not([data-label=""])::before {
        content: attr(data-label);
        display: block;
        font-size: 11px;
        font-weight: 600;
        color: var(--muted);
        margin-bottom: 2px;
    }

    /* Full-width inputs */
    .line-items-edit-table td input,
    .line-items-edit-table td select {
        width: 100% !important;
        box-sizing: border-box;
    }

    /* Thumbnail: inline at top. Higher specificity than the corner
       reservation below (0,2,1 vs 0,1,1), so a first cell that is a thumbnail
       or an include-checkbox — expense-detail's line editor and its
       receipt-scan preview — keeps its own width and reserves nothing. */
    .line-items-edit-table td[data-label=""]:first-child {
        display: inline-block;
        width: auto !important;
        padding-right: 0 !important;
    }

    /* Delete button: top-right corner */
    .line-items-edit-table td:last-child {
        position: absolute;
        top: 8px;
        right: 8px;
        width: auto !important;
    }

    /* ...and the card must RESERVE that corner. The rule above takes the last
       cell out of flow, so the first field runs the full width of the card and
       passes underneath the button. That was invisible while the button
       measured 25.1x18.0; when the 2026-09-04 sweep gave it the 44x44 touch
       target WCAG asks for, it covered 42.0x23.5px of the description input on
       invoice-detail and proposal-detail at both 375px and 320px — measured in
       the browser — so a tap aimed at the description deleted the line. 52px
       is the button's 44 plus its 8px offset. */
    .line-items-edit-table td:first-child {
        padding-right: 52px !important;
    }

    /* Amount row: visual separator (second-to-last cell) */
    .line-items-edit-table td:nth-last-child(2) {
        font-weight: 600;
        border-top: 1px solid var(--line);
        margin-top: 4px;
        padding-top: 6px !important;
    }

    /* ---- Multi-receipt preview: same card skeleton, different columns ----
       The receipt-scan preview table (#rp-multi-body, expense-detail.php) opts
       into .line-items-edit-table above so its controls stop rendering 47px and
       25px wide inside a 246px card. Its columns do not mean what a line
       editor's do, and two of the shared rules are positional:

         td:last-child        is a DELETE BUTTON in a line editor, so that rule
                              pins it to the card's top-right corner. Here it is
                              the AMOUNT: it must stay in flow, and it takes the
                              separator that marks the amount row elsewhere.
         td:nth-last-child(2) is the amount in a line editor. Here it is the VAT
                              select, which takes no emphasis.

       Corrected here rather than by loosening the shared rules, which four
       other tables depend on. Equal specificity, so this block must stay AFTER
       the one above — source order is the only thing deciding. */
    .receipt-multi-table td:last-child {
        position: static;
        width: 100% !important;
        text-align: left !important;
        font-weight: 600;
        border-top: 1px solid var(--line);
        margin-top: 4px;
        padding-top: 6px !important;
    }

    .receipt-multi-table td:nth-last-child(2) {
        font-weight: 400;
        border-top: 0;
        margin-top: 0;
        padding-top: 4px !important;
    }

    /* The include checkbox is not a text field: the shared
       `.line-items-edit-table td input { width:100% }` would stretch it. */
    .receipt-multi-table td input[type="checkbox"] {
        width: auto !important;
    }

    /* Distance calculator: stack vertically */
    #distance-panel > div {
        flex-direction: column;
    }
    #distance-panel > div > div {
        min-width: 0 !important;
    }

    /* Quick time entry: two-column grid on mobile */
    .app-card form > div[style*="flex-wrap"] > div {
        min-width: 0 !important;
        flex: 1 1 45%;
    }
}

/* ============================================================
   T018G — work-order time tables (Time Logged, Unlinked billable
   time) — mobile card layout, same pattern as .line-items-edit-table
   above. Plain inline tables were the last source of internal card
   overflow on a populated work order at 390px even after the page/
   grid-level overflow was fixed.
   ============================================================ */
@media (max-width: 640px) {
    .work-order-time-table thead,
    .work-order-unlinked-time-table thead {
        display: none;
    }

    .work-order-time-table,
    .work-order-time-table tbody,
    .work-order-time-table tr,
    .work-order-time-table td,
    .work-order-unlinked-time-table,
    .work-order-unlinked-time-table tbody,
    .work-order-unlinked-time-table tr,
    .work-order-unlinked-time-table td {
        display: block;
        width: 100%;
    }

    .work-order-time-table tr,
    .work-order-unlinked-time-table tr {
        border: 1px solid var(--line);
        border-radius: var(--radius-sm);
        padding: 10px;
        margin-bottom: 8px;
    }

    .work-order-time-table td,
    .work-order-unlinked-time-table td {
        padding: 3px 0 !important;
        text-align: left !important;
        white-space: normal !important;
        width: 100% !important;
    }

    /* Field labels via data-label, same convention as line-items-edit-table */
    .work-order-time-table td[data-label]:not([data-label=""])::before,
    .work-order-unlinked-time-table td[data-label]:not([data-label=""])::before {
        content: attr(data-label);
        display: block;
        font-size: 11px;
        font-weight: 600;
        color: var(--muted);
        margin-bottom: 2px;
    }

    /* Link action: full-width button instead of a cramped right-aligned cell */
    .work-order-unlinked-time-table td:last-child form {
        display: block !important;
    }
    .work-order-unlinked-time-table td:last-child button {
        width: 100%;
    }
}

/* ============================================================
   T018J — shared admin-table mobile card layout: /app/access
   Roles table, /app/access Member roles table, /app/members Users
   table. Same mechanism as .work-order-time-table above (itself
   modeled on .line-items-edit-table): thead hidden, each data row
   becomes a bordered card, cells get a data-label caption. A single
   colspan detail/edit row (the expandable rights/capacity panels,
   the custom-role edit form) has only one cell, so it already
   collapses to one full-width block under the same rules — no
   separate handling needed, and it keeps its own internal <strong>
   labels for context.
   ============================================================ */
@media (max-width: 640px) {
    .admin-mobile-table thead {
        display: none;
    }

    .admin-mobile-table,
    .admin-mobile-table tbody,
    .admin-mobile-table tr,
    .admin-mobile-table td {
        display: block;
        width: 100%;
    }

    .admin-mobile-table tr {
        border: 1px solid var(--line);
        border-radius: var(--radius-sm);
        padding: 10px;
        margin-bottom: 8px;
    }

    .admin-mobile-table td {
        padding: 3px 0 !important;
        text-align: left !important;
        white-space: normal !important;
        width: 100% !important;
    }

    .admin-mobile-table td[data-label]:not([data-label=""])::before {
        content: attr(data-label);
        display: block;
        font-size: 11px;
        font-weight: 600;
        color: var(--muted);
        margin-bottom: 2px;
    }
}

/* ============================================================
   Auth pages (login, reset-password)
   ============================================================ */
.auth-shell {
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    background: var(--bg);
}

.auth-header {
    height: var(--topbar-h);
    display: flex;
    align-items: center;
    padding: 0 24px;
    background: var(--paper);
    border-bottom: 1px solid var(--line);
    flex-shrink: 0;
}

.auth-brand {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    font-weight: 700;
    font-size: 16px;
    color: var(--accent);
    text-decoration: none;
}

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

.auth-brand__mark {
    width: 26px;
    height: 26px;
    border-radius: var(--radius-sm);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-weight: 800;
    font-size: 14px;
    color: #fff;
    background: var(--accent);
    flex-shrink: 0;
}

.auth-main {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 40px 20px;
}

.auth-body {
    width: 100%;
    max-width: 440px;
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.auth-footer {
    padding: 16px 24px;
    border-top: 1px solid var(--line);
    font-size: 12px;
    color: var(--muted);
    text-align: center;
    flex-shrink: 0;
}

/* Alert styles shared by auth pages */
.alerts {
    display: grid;
    gap: 8px;
}

.alert {
    padding: 10px 14px;
    border-radius: var(--radius-sm);
    font-size: 13px;
    font-weight: 500;
    border: 1px solid transparent;
}

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

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

/* Full-width button for auth forms */
.app-btn--block {
    width: 100%;
    justify-content: center;
}

/* ============================================================
   Records toolbar (view tabs + search)
   ============================================================ */
.records-toolbar {
    display: flex;
    gap: 8px;
    margin-bottom: 16px;
    align-items: center;
    flex-wrap: wrap;
}

.records-toolbar__views {
    display: flex;
    gap: 4px;
}

.records-toolbar__search {
    margin-left: auto;
}

.records-toolbar__search .app-input {
    width: 200px;
    padding: 4px 10px;
    font-size: 13px;
}

@media (max-width: 640px) {
    .records-toolbar {
        flex-direction: column;
        align-items: stretch;
        gap: 8px;
    }
    .records-toolbar__views {
        justify-content: stretch;
    }
    .records-toolbar__views .app-btn {
        flex: 1;
        justify-content: center;
    }
    .records-toolbar__label {
        display: none;
    }
    .records-toolbar__search {
        margin-left: 0;
    }
    .records-toolbar__search .app-input {
        width: 100%;
    }
}

/* ============================================================
   Kanban board
   ============================================================ */
.kanban-board {
    display: flex;
    gap: 12px;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    padding-bottom: 16px;
    min-height: 300px;
}

.kanban-col {
    min-width: 220px;
    max-width: 300px;
    flex: 1 0 220px;
    background: var(--bg);
    border-radius: var(--radius-sm);
    padding: 10px;
    transition: outline 0.15s;
}

.kanban-col.is-drag-over {
    outline: 2px dashed var(--accent);
    outline-offset: -2px;
}

.kanban-col-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 10px;
    padding-bottom: 8px;
    border-bottom: 1px solid var(--line);
}

.kanban-col-count {
    font-size: 12px;
    color: var(--muted);
}

.kanban-card {
    background: var(--paper);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    padding: 10px 12px;
    margin-bottom: 8px;
    cursor: grab;
    transition: opacity 0.15s, box-shadow 0.15s;
}

.kanban-card:hover {
    box-shadow: var(--shadow);
}

.kanban-card.is-dragging {
    opacity: 0.4;
}

.kanban-card-title {
    display: block;
    font-weight: 500;
    font-size: 13px;
    margin-bottom: 4px;
    text-decoration: none;
    color: var(--ink);
}

.kanban-card-title:hover {
    color: var(--accent);
}

.kanban-card-meta {
    font-size: 11px;
    color: var(--muted);
}

/* The tap route off a card. Desktop keeps drag-and-drop and shows nothing
   extra, so `display: none` is the base and the phone breakpoint below turns
   it on — see app/pages/records-kanban.php for the measurement that says why
   the phone needs it at all. `display` is set to flex there rather than block
   because min-height does nothing to an inline box, and an icon button that
   declares 44px and renders 19px is the defect this is fixing, not a fix. */
.kanban-card-move {
    display: none;
}

/* Was an inline `style` attribute written by the modal's own JavaScript.
   Same values; a stylesheet can reach these and an inline declaration could
   not. */
.kanban-modal-note {
    margin: 0 0 12px;
    font-size: 13px;
    color: var(--muted);
}

@media (max-width: 640px) {
    /* THE BOARD STACKS.

       Measured 2026-09-04 at both phone widths, before this rule: seven
       workflow states at a 200px minimum plus gaps made a 1472px board sitting
       inside a 343px (375px viewport) or 288px (320px) card. It scrolled
       inside its own box, so `documentElement.scrollWidth <= clientWidth` was
       true and every page-level overflow check reported the page clean — while
       1184px of board was hidden, ONE column of seven was visible at rest, and
       the rendered horizontal scrollbar was 0px tall. A phone has overlay
       scrollbars, so nothing on screen said the other six columns existed;
       they were reachable only by a reader who guessed to swipe sideways, six
       times over, to find «Valmis».

       Columns therefore become full-width rows and the board scrolls the way
       every page scrolls. `display: block` rather than a column flex is
       deliberate: the base rule's `gap` belongs to the row layout, and a
       stacked column carrying its own margin lets an empty column collapse to
       its own header instead of to a gap-sized hole.

       Desktop is untouched — the whole change lives inside this breakpoint,
       and the 641px/1280px assertions in
       tests/records_kanban_rendered_mobile_test.php hold the row layout in
       place. */
    .kanban-board {
        display: block;
        overflow-x: visible;
        padding-bottom: 0;
        min-height: 0;
    }
    .kanban-col {
        min-width: 0;
        max-width: none;
        flex: none;
        margin-bottom: 12px;
    }
    .kanban-col:last-child {
        margin-bottom: 0;
    }

    /* Card becomes a row: text on the left, the move button on the right. */
    .kanban-card {
        display: flex;
        align-items: flex-start;
        gap: 8px;
    }
    .kanban-card-main {
        flex: 1 1 auto;
        min-width: 0;   /* long Finnish titles must wrap, not push the button out */
    }
    .kanban-card-move {
        display: flex;
        align-items: center;
        justify-content: center;
        flex: 0 0 auto;
        width: 44px;
        height: 44px;
        min-width: 44px;
        min-height: 44px;
        margin: -4px -6px -4px 0;   /* eat the card's own padding, not the target */
        padding: 0;
        border: 1px solid var(--line);
        border-radius: var(--radius-sm);
        background: var(--bg);
        color: var(--ink);
        cursor: pointer;
        -webkit-tap-highlight-color: transparent;
    }
    .kanban-card-move .material-symbols-rounded {
        font-size: 20px;
        line-height: 1;
    }
    .kanban-card-move:focus-visible {
        outline: 2px solid var(--accent);
        outline-offset: 2px;
    }

    /* The destination list inside the shared transition modal.

       This rule deliberately declares NO min-height and NO font-size. The
       options carry `.app-btn`, and the phone block near the end of this file
       (`.app-btn { min-height: 44px; font-size: 14px }`) is a LATER
       declaration of equal specificity (0,1,0), so anything restated here
       would be dead the moment it disagreed — the same mechanism that killed
       `.app-btn--worker-action`'s 15px, documented at that rule. Measured: the
       options render 44px tall at 14px, and the 44px floor is asserted on the
       rendered box in tests/records_kanban_rendered_mobile_test.php rather
       than trusted from a declaration here. */
    .kanban-move-options {
        display: flex;
        flex-direction: column;
        gap: 8px;
    }
    .kanban-move-option {
        display: flex;
        align-items: center;
        justify-content: flex-start;
        width: 100%;
        text-align: left;
    }
}

/* ============================================================
   Calendar view
   ============================================================ */
.cal-nav {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-bottom: 16px;
    flex-wrap: wrap;
}

.cal-nav-title {
    font-weight: 600;
    font-size: 16px;
    min-width: 140px;
    text-align: center;
}

.cal-grid {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    gap: 1px;
    background: var(--line);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    overflow: hidden;
}

.cal-header {
    background: var(--bg);
    padding: 6px 2px;
    text-align: center;
    font-weight: 600;
    font-size: 11px;
    color: var(--muted);
}

.cal-day {
    background: var(--paper);
    min-height: 90px;
    padding: 4px;
    vertical-align: top;
    cursor: pointer;
}

.cal-day--other {
    opacity: 0.35;
}

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

.cal-day.is-selected {
    background: var(--accent-soft);
}

.cal-day-num {
    font-size: 12px;
    font-weight: 500;
    color: var(--muted);
    text-align: center;
    margin-bottom: 2px;
}

/* Dots: shown on mobile, hidden on desktop */
.cal-day-dots {
    display: none;
    justify-content: center;
    gap: 3px;
    padding: 2px 0;
}

.cal-dot {
    width: 5px;
    height: 5px;
    border-radius: 50%;
    background: var(--accent);
}

/* Pills: shown on desktop, hidden on mobile */
.cal-pill {
    display: block;
    font-size: 11px;
    padding: 1px 4px;
    border-radius: 3px;
    margin-bottom: 1px;
    text-decoration: none;
    color: var(--ink);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    background: var(--bg);
    border: 1px solid var(--line);
}

.cal-pill:hover {
    background: var(--accent-soft);
    border-color: var(--accent);
}

.cal-overflow {
    font-size: 10px;
    color: var(--muted);
    text-align: center;
}

/* Agenda list below the calendar */
.cal-agenda {
    margin-top: 16px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--paper);
}

.cal-agenda[hidden] {
    display: none;
}

.cal-agenda-header {
    padding: 10px 14px;
    font-weight: 600;
    font-size: 13px;
    border-bottom: 1px solid var(--line);
    background: var(--bg);
}

.cal-agenda-empty {
    padding: 12px 14px;
    font-size: 13px;
    color: var(--muted);
}

.cal-agenda-item {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 14px;
    border-bottom: 1px solid var(--line);
    text-decoration: none;
    color: var(--ink);
    font-size: 13px;
}

.cal-agenda-item:last-child {
    border-bottom: none;
}

.cal-agenda-item:hover {
    background: var(--accent-soft);
}

.cal-agenda-item .app-badge {
    flex-shrink: 0;
}

/* Mobile: dots + compact cells */
@media (max-width: 640px) {
    .cal-day {
        min-height: 44px;
        padding: 2px;
    }
    .cal-day-num {
        font-size: 11px;
    }
    .cal-day--today .cal-day-num {
        width: 20px;
        height: 20px;
        font-size: 11px;
    }
    .cal-day-dots {
        display: flex;
    }
    .cal-pill,
    .cal-overflow {
        display: none;
    }
    .cal-nav-title {
        font-size: 14px;
        min-width: 120px;
    }
}

/* ============================================================
   Lookup typeahead
   ============================================================ */
.lookup-wrapper {
    position: relative;
}

.lookup-results {
    position: absolute;
    z-index: 20;
    top: 100%;
    left: 0;
    right: 0;
    background: var(--paper);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    max-height: 240px;
    overflow-y: auto;
    box-shadow: var(--shadow);
}

.lookup-item {
    padding: 8px 12px;
    cursor: pointer;
    font-size: 13px;
}

.lookup-item:hover {
    background: var(--accent-soft);
}

.lookup-empty {
    padding: 8px 12px;
    color: var(--muted);
    font-size: 13px;
}

/* Suggest-enabled lookups (work orders): a header row above the recent list
   and a two-line row — title plus a muted context line that keeps duplicate
   titles distinguishable. Context truncates rather than wraps so the dropdown
   stays compact on a 390px screen. */
.lookup-suggest-header {
    padding: 8px 12px 4px;
    color: var(--muted);
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.lookup-item-title {
    font-size: 13px;
}

.lookup-item-context {
    color: var(--muted);
    font-size: 11.5px;
    margin-top: 1px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* ============================================================
   Toast notifications
   ============================================================ */
.toast-container {
    position: fixed;
    top: 16px;
    right: 16px;
    z-index: 9000;
    display: flex;
    flex-direction: column;
    gap: 8px;
    pointer-events: none;
}

.app-toast {
    padding: 10px 16px;
    border-radius: var(--radius-sm);
    font-size: 13px;
    box-shadow: var(--shadow);
    pointer-events: auto;
    max-width: 320px;
    animation: toast-in 0.2s ease;
}

.app-toast--out {
    animation: toast-out 0.3s ease forwards;
}

.app-toast--danger  { background: var(--danger-soft);  color: var(--danger);  border: 1px solid var(--danger); }
.app-toast--success { background: var(--success-soft); color: var(--success); border: 1px solid var(--success); }
.app-toast--warning { background: var(--warning-soft); color: var(--warning); border: 1px solid var(--warning); }

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

/* ============================================================
   Kanban transition modal
   ============================================================ */
.kanban-modal-overlay {
    position: fixed;
    inset: 0;
    background: rgba(15, 23, 42, 0.4);
    z-index: 8000;
    display: flex;
    align-items: center;
    justify-content: center;
}

.kanban-modal-box {
    background: var(--paper);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-lg);
    padding: 24px;
    width: 100%;
    max-width: 360px;
}

.kanban-modal-box h3 {
    margin: 0 0 16px;
    font-size: 15px;
}

.kanban-modal-footer {
    display: flex;
    gap: 8px;
    justify-content: flex-end;
    margin-top: 20px;
}

/* ============================================================
   Kanban warning badge
   ============================================================ */
.kanban-card-warn {
    font-size: 12px;
    color: var(--warning);
    margin-left: 4px;
    cursor: default;
}

/* ============================================================
   Records list table — desktop base.

   These three declarations used to be inline `style` attributes in
   app/pages/records.php. Identical values, so desktop is unchanged;
   the point of moving them is that an inline declaration outranks
   every stylesheet rule, so while they lived in the attribute no
   breakpoint could resize or restyle those cells.
   ============================================================ */
.records-table-wrap {
    padding: 0;
    overflow-x: auto;
}
.records-table__amount {
    text-align: right;
    font-size: 13px;
}
.records-table__meta {
    font-size: 12px;
}
.records-table__updated {
    font-size: 12px;
    color: var(--muted);
}

/* ============================================================
   Records list table — mobile: one card per record.

   WAS: every column except the name and the status was set to
   `display: none` at this breakpoint. The table then fitted a phone,
   but at the cost of the data -- the customer register showed Nimi
   and Tila and silently dropped Y-tunnus, Asiakastyyppi, Sähköposti,
   Omistaja and Päivitetty; an invoice list dropped its total.

   NOW: the generic renderer's table opts into T018J's
   .admin-mobile-table (thead hidden, each row a bordered card, each
   cell captioned by its data-label). Same mechanism as /app/access,
   /app/members and the work-order time table -- no second one. The
   rules below only add what is specific to this table: the outer
   .app-card chrome is dropped so cards do not sit inside a second
   frame, the primary column reads as the card's heading, and long
   unbroken values (an e-mail address) wrap instead of pushing the
   page sideways.

   The work-order-shaped types are unaffected: .records-table-wrap--wo
   still hides this table entirely in favour of .app-wo-card-list.
   ============================================================ */
@media (max-width: 640px) {
    .records-table-wrap {
        padding: 0;
        overflow-x: hidden;
        background: transparent;
        border: 0;
        box-shadow: none;
    }

    .records-table.admin-mobile-table tr {
        background: var(--paper);
    }

    /* Primary column = the card's title. No caption above it (its
       data-label is deliberately empty), and large enough to be the
       obvious tap target on the card. */
    .records-table.admin-mobile-table td:first-child {
        font-size: 15px;
        font-weight: 600;
        line-height: 1.35;
        padding-bottom: 6px !important;
    }

    /* The record link is the card's only way in. As inline text it was a
       19px-tall target; as a block it fills the title cell and clears the
       44px touch minimum this renderer adopted in the previous slice. */
    .records-table.admin-mobile-table td:first-child .app-table-link {
        display: flex;
        align-items: center;
        min-height: 44px;
    }

    /* One size for every value on the card. Without this the cells
       inherit `.app-table th, .app-table td { font-size: 12px }` from
       the (max-width: 900px) block, which is desktop-table density and
       too small to read on a phone, while the three cells that carry
       .records-table__meta / __updated / __amount would sit at their
       own 12/13px -- three sizes in one card.

       overflow-wrap: a long e-mail or reference is one unbroken token;
       without this it widens the card and the page scrolls sideways. */
    .records-table.admin-mobile-table td {
        font-size: 14px;
        overflow-wrap: anywhere;
    }

    /* The cell dividers are the T018J pattern's; only the last one is
       dropped, because it would double up with the card's own border. */
    .records-table.admin-mobile-table td:last-child {
        border-bottom: 0;
    }
}

/* Work-order list: card layout on mobile, table on desktop */
.app-wo-card-list { display: none; }
.records-status-select { display: none; }

.records-status-filters {
    display: flex;
    margin-bottom: 16px;
}
.records-status-chips {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
}

/* Focus ring: remove browser default on job cards (tap artifact on mobile) */
.app-job-card:focus { outline: none; }
.app-job-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.app-job-card { -webkit-tap-highlight-color: transparent; }

@media (max-width: 640px) {
    /* Work order list: swap table for cards */
    .app-wo-card-list {
        display: flex;
        flex-direction: column;
        gap: 8px;
    }
    .records-table-wrap--wo { display: none; }

    /* View toggle: hide the individual buttons for mobile-card types (cards
       replace table; kanban/calendar not useful on a phone) — but a button can
       opt in with .app-btn--keep-mobile (Viikkokirjaus is the worker's primary
       mobile surface, and Taulukko is the way back from it). */
    .records-toolbar--wo .records-toolbar__views .app-btn { display: none; }
    .records-toolbar--wo .records-toolbar__views .app-btn--keep-mobile {
        display: inline-flex;
        flex: 1;
        justify-content: center;
    }
    .records-toolbar--wo .app-btn--keep-mobile .records-toolbar__label { display: inline; }

    /* Status filter: hide chips, show select */
    .records-status-filters--wo .records-status-chips { display: none; }
    .records-status-filters--wo .records-status-select {
        display: block;
        width: 100%;
        font-size: 14px;
        padding: 6px 10px;
    }

    /* Tighten top chrome on records pages */
    .app-page-header { margin-bottom: 12px; }
    .records-status-filters--wo { margin-bottom: 10px; }
    .records-toolbar--wo { margin-bottom: 8px; }
}

/* Job status row: 2-column grid on mobile */
@media (max-width: 640px) {
    .app-job-status-row {
        grid-template-columns: 1fr 1fr;
    }
    .app-job-status-item {
        border-right: none;
        border-bottom: 1px solid var(--line);
    }
    .app-job-status-item:nth-child(odd)  { border-right: 1px solid var(--line); }
    .app-job-status-item:nth-child(3),
    .app-job-status-item:nth-child(4)    { border-bottom: none; }
}

/* ============================================================
   Call Queue — mobile-first dialer styles
   ============================================================ */

/* Stats bar */
.cq-stats {
    display: flex;
    gap: 12px;
    flex-wrap: wrap;
    margin-bottom: 16px;
    padding: 12px 16px;
    background: var(--paper);
    border-radius: var(--radius-md);
    border: 1px solid var(--line);
}
.cq-stat { display: flex; flex-direction: column; align-items: center; min-width: 50px; }
.cq-stat-value { font-size: 20px; font-weight: 700; color: var(--ink); }
.cq-stat-label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: .03em; }
.cq-stat--danger .cq-stat-value { color: var(--danger); }
.cq-stat--accent .cq-stat-value { color: var(--accent); }
.cq-stat--success .cq-stat-value { color: var(--success); }

/* Current lead card */
.cq-current-card {
    background: var(--paper);
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
    padding: 20px;
    max-width: 500px;
    margin: 0 auto;
    box-shadow: var(--shadow);
}
.cq-current-header {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    margin-bottom: 12px;
}
.cq-current-company {
    font-size: 20px;
    font-weight: 700;
    margin: 0;
    line-height: 1.3;
}
.cq-current-contact {
    font-size: 14px;
    color: var(--muted);
    margin-top: 2px;
}
.cq-deal-value {
    font-size: 18px;
    font-weight: 700;
    color: var(--success);
    white-space: nowrap;
}

/* Last note */
.cq-last-note {
    display: flex;
    align-items: flex-start;
    gap: 6px;
    font-size: 13px;
    color: var(--muted);
    background: var(--bg);
    border-radius: var(--radius-sm);
    padding: 8px 10px;
    margin-bottom: 16px;
    line-height: 1.4;
}

/* Call button */
.cq-call-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    width: 100%;
    padding: 14px 20px;
    font-size: 18px;
    font-weight: 600;
    color: #fff;
    background: var(--success);
    border-radius: var(--radius-md);
    text-decoration: none;
    margin-bottom: 12px;
    transition: background 0.15s;
}
.cq-call-btn:hover { background: #15803d; }
.cq-call-btn .material-symbols-rounded { font-size: 22px; }

/* Contact secondary row (SMS, copy, email) */
.cq-contact-actions { margin-bottom: 16px; }
.cq-contact-secondary {
    display: flex;
    gap: 6px;
    justify-content: center;
    margin-top: 8px;
    flex-wrap: wrap;
}

/* Fast dispositions (one-click, top row) */
.cq-fast-dispositions {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 6px;
    margin-bottom: 8px;
}

/* Disposition buttons */
.cq-dispositions {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 6px;
    margin-bottom: 12px;
}
@media (min-width: 480px) {
    .cq-dispositions { grid-template-columns: repeat(4, 1fr); }
}

.cq-disp-btn--accent .material-symbols-rounded { color: var(--accent); }
.cq-disp-btn {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    padding: 10px 4px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--paper);
    cursor: pointer;
    font-size: 11px;
    font-weight: 500;
    color: var(--ink);
    transition: all 0.15s;
}
.cq-disp-btn:hover { border-color: var(--accent); }
.cq-disp-btn .material-symbols-rounded { font-size: 20px; }
.cq-disp-btn--selected { border-color: var(--accent); background: var(--accent-soft); }
.cq-disp-btn--success .material-symbols-rounded { color: var(--success); }
.cq-disp-btn--warning .material-symbols-rounded { color: var(--warning); }
.cq-disp-btn--muted .material-symbols-rounded { color: var(--muted); }
.cq-disp-btn--info .material-symbols-rounded { color: var(--accent); }
.cq-disp-btn--danger .material-symbols-rounded { color: var(--danger); }

/* (disposition grid responsive handled above) */

/* Upcoming queue list */
.cq-upcoming { display: flex; flex-direction: column; }
.cq-upcoming-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 8px 0;
    border-bottom: 1px solid var(--line);
    text-decoration: none;
    color: var(--ink);
    font-size: 13px;
    transition: background 0.1s;
}
.cq-upcoming-item:last-child { border-bottom: none; }
.cq-upcoming-item:hover { background: var(--bg); }
.cq-upcoming-main { display: flex; flex-direction: column; gap: 1px; }
.cq-upcoming-company { font-weight: 600; }
.cq-upcoming-contact { font-size: 12px; color: var(--muted); }
.cq-upcoming-meta { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }

/* Mobile: tighter spacing */
@media (max-width: 480px) {
    .cq-current-card { padding: 16px; }
    .cq-current-company { font-size: 18px; }
    .cq-call-btn { font-size: 16px; padding: 12px 16px; }
    .cq-disp-btn { padding: 8px 2px; font-size: 10px; }
}

/* ============================================================
   Voucher line entry on a phone (.mv-lines)
   ============================================================
   P2 (19.8.2026): at 390px the muistiotosite line table measured
   ~540px wide with memo inputs ending past the viewport edge —
   desktop-shaped rows are unusable for typing on a phone. Below
   640px each rivi becomes a stacked card: the thead hides and
   every cell announces itself with the SAME text via data-label.
   This block and the data-label attributes in the muistiotosite
   region of app/pages/accounting.php are one relation — the suite
   reads it from both sides. Desktop layout is untouched. */
@media (max-width: 640px) {
    .mv-lines thead {
        display: none;
    }

    .mv-lines,
    .mv-lines tbody,
    .mv-lines tr,
    .mv-lines td {
        display: block;
        width: 100%;
    }

    .mv-lines tr {
        border: 1px solid var(--line);
        border-radius: 8px;
        padding: 8px 12px 12px;
        margin-bottom: 10px;
    }

    .mv-lines td {
        border-bottom: none;
        padding: 6px 0 0;
    }

    .mv-lines td::before {
        content: attr(data-label);
        display: block;
        font-size: 11px;
        font-weight: 600;
        text-transform: uppercase;
        color: var(--muted);
        margin-bottom: 2px;
    }

    /* "Rivi 1" reads as the card's own heading, on one line. */
    .mv-lines td:first-child {
        font-weight: 700;
        padding-top: 0;
    }

    .mv-lines td:first-child::before {
        display: inline;
        margin-right: 4px;
    }

    /* Every input and select fills the card; the amount field's
       inline desktop width (120px) must not survive on a phone.

       40 -> 44 (3.9.2026), DECLARED. The field tap-target rule further
       down resolves to 44px here and outranks this line (0,8,1 against
       0,2,1), so this 40 had become a dead number that still READ like
       a promise — delete the newer rule and the memo voucher would
       silently fall back to 40 while every other field stayed 44. Same
       selector, same property, a HIGHER floor: the set of trees this
       accepts is strictly smaller than before. No test pinned this
       literal (grep 'min-height: 40px' over tests/ matches only two
       page-region pins for an unrelated inline style). */
    .mv-lines td .app-input {
        width: 100% !important;
        max-width: 100%;
        box-sizing: border-box;
        min-height: 44px;
    }
}

/* ============================================================
   Accounting workspace pages on a phone (.app-shell--own-nav)
   ============================================================
   KIRJANPITO UI SLICE A (28.9.2026) — THE RAIL IS THE DRAWER. The
   19.8 band put the strip's way back, company and identity above
   the topbar because the strip then held no navigation; the rail now
   holds the workspace's destinations, so on a phone it is the
   ordinary off-canvas drawer behind the menu button, exactly like a
   platform page's. Opening it covers the page instead of pushing it
   down, and the identity footer lives inside the drawer — it can no
   longer overlap the topbar's "Kirjaudu ulos", which is the overlap
   the whole-product review measured at 390 and 320 px. The platform
   rules above already make .app-sidebar the drawer; this block keeps
   the two tap-target rules the strip pages carried. */
@media (max-width: 900px) {
    /* THE WAY OUT OF A STRIP PAGE IS A TAP TARGET (3.9.2026).
       Measured at 375x812: the way back stood 18px tall, less than HALF
       the 44px iOS/WCAG minimum. In the drawer it is still a way out of
       the workspace, so it keeps its 44px. min-height, not height: the
       label is Finnish and wraps on a narrow phone. */
    .app-shell--own-nav .app-sidebar--strip .app-sidebar-return {
        min-height: 44px;
    }

    /* AND SO IS THE WAY OUT OF THE SESSION. "Kirjaudu ulos" measured
       19.5px inside a collapsed topbar on strip pages. Scoped to strip
       pages — a platform page's topbar is another lane's call, and the
       control assertion in tests/accounting_touch_targets_test.php pins
       that scope. */
    .app-shell--own-nav .app-topbar-right a {
        display: inline-flex;
        align-items: center;
        min-height: 44px;
    }
}

/* ============================================================
   Status tables on a phone (.app-table--cards)
   ============================================================
   P2 (19.8.2026): the coverage, packages and activation status
   tables were desktop-shaped at 390px — up to ~270px wider than
   their cards, clipped without any scrollbar because the card
   crops silently. A table marked app-table--cards stacks below
   640px: the thead hides and every row becomes a card whose
   cells announce their column via data-label, the SAME text as
   the column heading. Same treatment as .mv-lines, kept separate
   because the line EDITOR sizes inputs and this one styles
   read-only text. Desktop layout is untouched behind the media
   query. The pins in tests/gl_tyopoyta_test.php read markup and
   css as one relation. */

/* ============================================================
   Column labels that are CONTENT, not an attribute
   ============================================================
   4.9.2026. The bank page's proposal queue was an eight-column
   table 687px wide inside a 341px card at 375x812 — measured, and
   identically 687px inside 286px at 320px. Four columns (Summa,
   Ehdotettu vastatili, Miksi odottaa, Mistä ehdotus tulee) were
   entirely off screen at both widths, Vastapuoli was clipped, and
   at no horizontal scroll position were the row NUMBER and the
   reason that row WAITS on screen together. The table scrolled
   inside its own card, so the document itself reported no
   overflow and nothing on the screen said the columns existed.

   It stacks with the .app-table--cards rules below; only the
   label SOURCE differs. That region is under
   accounting_queue_link_test §3.2, which forbids every attribute
   of the hook family inside it, so the column name is written in
   the markup where a Finnish reviewer reads it, instead of being
   interpolated out of an attribute by the stylesheet. That is
   also the better place for it: a column heading is content.

   DECLARED BEFORE THE MEDIA QUERY ON PURPOSE. Both rules carry
   the same specificity as their phone counterparts, so the later
   declaration wins; declared after, the hidden state would have
   won at every width and the labels would never have appeared.
   Measured, not reasoned: the first build of this did exactly
   that and the stacked cards rendered unlabelled. */
.app-table-cardlabel {
    display: none;
}

.app-table-amount {
    text-align: right;
}

@media (max-width: 640px) {
    .app-table--cards thead {
        display: none;
    }

    .app-table--cards,
    .app-table--cards tbody,
    .app-table--cards tr,
    .app-table--cards td {
        display: block;
        width: 100%;
    }

    .app-table--cards tr {
        border: 1px solid var(--line);
        border-radius: 8px;
        padding: 8px 12px 10px;
        margin-bottom: 10px;
    }

    .app-table--cards td {
        border-bottom: none;
        padding: 6px 0 0;
    }

    .app-table--cards td::before {
        content: attr(data-label);
        display: block;
        font-size: 11px;
        font-weight: 600;
        text-transform: uppercase;
        color: var(--muted);
        margin-bottom: 2px;
    }

    .app-table--cards td:first-child {
        padding-top: 0;
    }

    /* A cell with no label used to get an empty ::before block and its
       2px margin anyway. Cells that carry their label in the MARKUP
       (below) get theirs from the span, so the attribute label is
       suppressed for both rather than stacking two label boxes. */
    .app-table--cards td:not([data-label]):not(.app-table-cardnum)::before {
        display: none;
    }

    /* THE ROW NUMBER IS THE CARD'S OWN HEADING. Its cell holds the
       anchor and nothing else — accounting_queue_link_test §5.4 pins
       that shape, so the "#" cannot be a span beside it. It is the same
       single glyph the desktop column heading uses, not prose. */
    .app-table--cards td.app-table-cardnum::before {
        content: "#";
        display: block;
        font-size: 11px;
        font-weight: 600;
        text-transform: uppercase;
        color: var(--muted);
        margin-bottom: 2px;
    }

    /* An amount right-aligned inside a full-width stacked block lands a
       column away from the label that names it. Left in a card, right in
       a column. */
    .app-table--cards td.app-table-amount {
        text-align: left;
    }

    .app-table-cardlabel {
        display: block;
        font-size: 11px;
        font-weight: 600;
        text-transform: uppercase;
        color: var(--muted);
        margin-bottom: 2px;
    }
}

/* ============================================================
   THE QUEUE'S TEXT SIZE, MOVED OUT OF THE MARKUP (4.9.2026)
   ============================================================
   The stacking fix made all eight columns of the bank queue
   REACHABLE on a phone. It did not make them READABLE: every one
   of that region's eleven text elements still carried its size as
   an INLINE style written by PHP — `font-size:13px` on six cells,
   `font-size:12px` on the two cells that say why a row waits and
   where the proposal came from, and on the region's three
   paragraphs.

   AN INLINE DECLARATION IS UNREACHABLE BY ANY STYLESHEET. No media
   query could ever have grown this region, at any width, however
   it was written; the columns were found and then read at 12px.
   That is why the size lives here now and not in the markup.

   ABOVE 640px THESE REPRODUCE THE INLINE VALUES EXACTLY — 13px for
   the cells and the empty-list line, 12px for the two note cells
   and the two muted paragraphs — so the desktop and tablet layout
   is the one it was, including the 900px band, which these beat on
   specificity exactly as the inline styles did.

   BELOW 640px the table has stacked into cards and there is a whole
   screen width per value, so the text becomes 16px.

   NO IMPORTANCE OVERRIDE IS USED OR NEEDED: one extra class on the card
   raises every selector here above `.app-table th, .app-table td`
   on its own. Declared AFTER the 640px cards block on purpose —
   gl_tyopoyta_test matches
   `@media (max-width: 640px) {[^@]*.app-table--cards thead` and a
   second 640px query opened before that block would hand it a
   second match.
   ============================================================ */
.app-card--queue .app-table td {
    font-size: 13px;
}

.app-card--queue .app-table td.app-queue-note {
    font-size: 12px;
}

.app-card--queue p {
    font-size: 12px;
}

.app-card--queue p.app-queue-empty {
    font-size: 13px;
}

@media (max-width: 640px) {
    .app-card--queue .app-table td,
    .app-card--queue .app-table td.app-queue-note,
    .app-card--queue p,
    .app-card--queue p.app-queue-empty {
        font-size: 16px;
    }
}
/* ============================================================
   Touch targets on accounting phone views
   ============================================================
   P3 (19.8.2026): the accounting navigation and report links
   measured 26-28px tall at 390px with 26-35px between centres —
   usable with care, easy to mis-tap. On strip pages
   (.app-shell--own-nav) every BLOCK-LEVEL link and summary in
   the content area now stands at least 40px tall on a phone;
   the paddings recentre the text inside the taller box and must
   beat the links' inline 3-4px paddings, hence !important.
   Inline links in running prose are untouched on purpose:
   min-height does not apply to inline boxes. Desktop stays
   compact behind the media query (the instrument's control
   measures the first content link under 40px at 1280px).

   RAISED 40 -> 44 (3.9.2026). The 40px bar was this project's own
   milestone number, not the platform's: iOS HIG and WCAG 2.5.5 both
   name 44. Measured at 375x812 with the stylesheet confirmed serving,
   the front page's 27 navigation tiles — the grid an entrepreneur
   lands on — rendered at exactly 42px: clearing our bar and missing
   the real one by 2px. The paddings go 13 -> 14 with it so 14+16+14
   is 44 on its own and the floor is a floor rather than the thing
   doing the work. The assertion in tests/gl_tyopoyta_test.php §7d
   that pinned the literal "min-height: 40px" was RAISED to 44 in the
   same commit, deliberately and in the strengthening direction; see
   tests/accounting_touch_targets_test.php for the falsifier. */
@media (max-width: 640px) {
    .app-shell--own-nav .app-main a,
    .app-shell--own-nav .app-main summary {
        min-height: 44px;
        padding-top: 14px !important;
        padding-bottom: 14px !important;
        box-sizing: border-box;
    }

    /* Inline links cannot take min-height; the paddings alone lift
       their tap box past 44px without shifting the line they sit in (the
       smallest measured inline link was 13px + a 26px pad short of the
       mark at 12px). The buttons are inline-blocks, so min-height is
       enough. */
    .app-shell--own-nav .app-main .app-btn {
        min-height: 44px;
    }
}

/* ============================================================
   Phone: a form field is a touch target too
   ============================================================
   3.9.2026. After the block above, NO link, button or summary on
   any accounting surface was under 44px — and 127 controls still
   were. Every one of them was an `input` or a `select`. Measured
   at 375x812 with app.css confirmed serving (366 rules, Inter):
   muistiotosite 48, alv 23, pankki 22, tiliöintiehdotukset 13,
   and the fy/view/period row's 5 on each of the other four.

   Two populations, one cause each:
     1. `.app-input`/`.app-select` fields at 38-40px — 16px font
        (from the zoom rule above) plus 8px paddings and a border.
        Short of 44 by 4-6px, never by design; nothing declares a
        height at all.
     2. Fields carrying NO class at 21-25px — the statement-import
        form on pankki and the suggestion rows — which get the 16px
        font but none of `.app-input`'s padding. A file input is the
        smallest of them at 21px.

   WHY min-height AND NOTHING ELSE. Growing a field's box is where
   horizontal overflow lives: these fields carry inline `width:` and
   `min-width:` declarations (measured: `width:120px`, `min-width:190px`,
   `width:100%; max-width:640px`), and zero horizontal overflow at
   375px AND 320px is a property this project already holds on all
   eight surfaces. `min-height` is a purely vertical metric, so it
   cannot move a row sideways — no padding change, no `width`, no
   `min-width`, nothing that participates in intrinsic sizing. The
   box-sizing is already `border-box` globally (line 64), so 44px is
   the outer box and the UA centres a single-line field's text in it.

   SCOPE, and why it is not the element selector the zoom rule uses:
   the zoom rule is global because iOS focus-zoom is a global defect.
   This one is scoped to `.app-shell--own-nav .app-main` — the
   accounting surfaces, the eight this slice measured. The platform
   lane has its own sub-44px controls (`.app-sidebar-view-as`, inline
   `font-size:11px`) and they want their own slice with their own
   browser proof, not to be swept along by a rule verified elsewhere.

   THE TEXTAREA IS A SEPARATE SELECTOR, and the reason is the one way
   this whole block could have made the product worse. `.app-textarea`
   declares `min-height: 80px` at LOWER specificity than an own-nav
   chain, so folding `textarea` into the selector above would have
   LOWERED that floor from 80px to 44px. `min-height` only ever raises
   a box, never shrinks one — but replacing an 80px floor with a 44px
   floor is a shrink, and it would have been invisible in a "count of
   controls under 44px". So textarea is matched with `:not(.app-textarea)`:
   the accounting pages' own textareas carry `class="app-input"` or no
   class at all (`lines_text`, `evidence`, `rationale`, `balances_text`,
   `gl-entry-json`), and a bare `rows="2"` box is ~42px on a phone —
   the same defect, on a branch none of the eight surfaces rendered in
   the synthetic tenant. Proved by resolving the cascade on an injected
   textarea in a real accounting page rather than claimed from a
   surface that has none.

   NOT COVERED, deliberately:
   - checkbox / radio / range / color — the box IS the visual, so
     growing it is a redraw of the control, not a hit-area change, and
     the right fix is a designed label hit area rather than a blanket
     min-height. Measured 3.9.2026: NONE of the eight surfaces rendered
     a checkbox or a radio at all. app/pages/accounting.php does hold
     two (`open_item_decision`, ~line 8078) on a branch the synthetic
     tenant did not reach, so they could not be measured here; they are
     named rather than swept in.
   - `hidden` / `image` — not rendered as a field.

   Pinned by tests/accounting_field_targets_test.php, which resolves
   the cascade rather than grepping for "44px", and reads the markup
   side to prove no field has grown an inline height that would
   outrank this rule. */
@media (max-width: 640px) {
    .app-shell--own-nav .app-main input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]):not([type="image"]),
    .app-shell--own-nav .app-main select,
    .app-shell--own-nav .app-main textarea:not(.app-textarea) {
        min-height: 44px;
    }
}

/* ============================================================
   Phone: the receipt goes in through THIS form
   ============================================================
   4.9.2026. The block above is scoped to `.app-shell--own-nav` —
   the accounting surfaces — and it says in as many words that the
   platform lane "wants their own slice with their own browser
   proof". This is that slice, for exactly one form: the upload on
   /app/documents, which is the ONLY door a photographed receipt
   enters the product through. `veksi_gl_add_evidence_choices()`
   sends the person holding a receipt to view=documents, and that
   screen sends them here to hand over the bytes.

   MEASURED at 375x812 and again at 320x812, logged in as an
   ordinary entrepreneur on a synthetic tenant, app.css confirmed
   serving (371 rules): all FIVE visible controls under 44px —
   file 253x21, category 135x21, record 295x21 (254x21 at 320),
   notes 233x24, the Lataa button 68x34. Five of five, on the first
   five taps of the task. The accounting half of the same walk
   (`Liitä aineistoksi`) already measures 44px on every control, so
   the defect is not "phones are hard", it is one screen that was
   never measured.

   WHY THE MARKUP CHANGED TOO, and why that is the fix rather than
   tidying: the form described its box in an inline `style=`
   (`display:flex`, `min-width:220px`, `flex:1; min-width:160px`).
   An inline declaration outranks every media query that is not
   `!important` — this file already records that fact about the
   statement paste box. So the desktop layout is restated as a
   CLASS here, DECLARED BEFORE the phone query, and the phone query
   then wins on the ordinary cascade. The alternative was a fourth
   `!important`; declaring the base first is the same fact used in
   the other direction.

   ORDER IS LOAD-BEARING. A sibling slice shipped a layout that
   looked exactly like success because a later declaration of equal
   specificity beat its media query. `.doc-upload` and its two
   modifiers are declared immediately below, and the
   `@media (max-width: 640px)` block after them; nothing may be
   inserted between.

   THE FILE INPUT'S FONT is raised here even though the global
   16px zoom rule excludes `[type="file"]` deliberately — iOS does
   not focus-zoom a file input, so that exclusion is about zoom and
   this is about reading the filename you just picked, which is the
   only confirmation that you chose the right photograph. Raising a
   font-size can never re-introduce focus-zoom, and the rule is
   scoped to this one form rather than reopening the global one.

   NOT DONE HERE, on purpose: the three fields still carry no
   visible label (only `notes` has a placeholder). That is new
   Finnish copy, so it is in the review queue rather than shipped
   unreviewed. */
.doc-upload {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    align-items: center;
}
.doc-upload__wide { min-width: 220px; }
.doc-upload__grow { flex: 1; min-width: 160px; }
/* The upload fields' own labels (slice D): a small caption before each control,
   on its own line on a phone, beside it in the desktop row. */
.doc-upload__label {
    font-size: 12px;
    font-weight: 600;
    color: var(--muted);
}

@media (max-width: 640px) {
    .doc-upload {
        display: block;
    }
    .doc-upload > input:not([type="hidden"]),
    .doc-upload > select,
    .doc-upload > button {
        display: block;
        width: 100%;
        min-width: 0;
        min-height: 44px;
        box-sizing: border-box;
        margin: 0 0 8px;
    }
    .doc-upload > input[type="file"] {
        font-size: 16px;
    }
    .doc-upload > .doc-upload__label {
        display: block;
        margin: 0 0 4px;
    }
    .doc-upload > button {
        justify-content: center;
        margin-bottom: 0;
    }
}

/* ============================================================
   Phone: the payroll page's own fields
   ============================================================
   4.9.2026. /app/palkkailmoitukset is the entrepreneur-facing half
   of payroll — the ONLY page from which a person, an employment and
   its effective-dated terms are recorded (the three `supported`
   functions of `palkanlaskenta` that carry `access => page`).

   IT IS NOT A STRIP PAGE. It never sets `$page_brings_own_nav`, so
   the shell is a bare `.app-shell` and every
   `.app-shell--own-nav .app-main` rule above misses it. The eight
   surfaces those rules were measured on did not include this one, so
   this page inherited the 16px zoom floor (written on the ELEMENT,
   therefore global) but none of the 44px floors (written on the
   own-nav chain). The split is visible in the measurement below:
   zoom risk zero, tap targets nil.

   MEASURED at 375x812 and again at 320x812, logged in as an ordinary
   entrepreneur on a synthetic tenant, app.css confirmed serving
   (95152 bytes over HTTP, not a 404 reporting beautiful numbers):

     view=henkilot, one person recorded   15 of 15 content controls <44px
     view=verokortit                       4 of 4
     view=poissaolot                       2 of 2
     view=outbox / syoteaineisto / maksuerat   2 each
       .app-input text + date fields   38-40px  (16px font + 8px pads)
       .app-input selects              40px
       .app-btn submit buttons         33-34px  (the smallest)

   Every control on the first-employee walk, at both widths. The four
   further sub-44 controls on the same screens — two sidebar links at
   35.5px, the 40px menu toggle, the 19.5px logout link — are platform
   chrome OUTSIDE `.payroll-page`, and the own-nav block above reserves
   them for their own slice. This rule cannot reach them: the wrapper
   in app/pages/palkkailmoitukset.php encloses `$content` only.

   MIN-HEIGHT AND NOTHING ELSE, for the reason the own-nav block
   already records: growing a field's box is where horizontal overflow
   lives, and zero horizontal overflow at 375px AND 320px is a property
   this page already holds (measured: none, on all eight views — the
   one wide table scrolls inside its own `overflow-x:auto` card, which
   is the correct pattern and was left alone). `min-height` is purely
   vertical, so it cannot move a row sideways. `box-sizing` is already
   `border-box` globally (line 64), so 44px is the outer box.

   THE TEXTAREA IS A SEPARATE SELECTOR for the same self-attack the
   own-nav block names: `.app-textarea` promises `min-height: 80px` at
   lower specificity, so folding `textarea` in with `input` would
   REPLACE an 80px floor with a 44px one — a shrink, and invisible to
   any "count of controls under 44px". All five textareas this page
   renders carry `class="app-input"`, never `app-textarea`, and a
   `rows="2"` box is ~42px. They sit behind an employment record the
   synthetic tenant had not reached, so they are covered by resolving
   the cascade rather than claimed from a rendered surface.

   NOT COVERED, deliberately: links. Unlike the accounting surfaces,
   this page rendered NO content link under 44px at either width — the
   sub-44 links here are all chrome. Nothing to fix, so nothing is
   swept in. Checkbox / radio / range / color / file: this page renders
   none at all (its inputs are text, date, hidden and submit only).

   Pinned by tests/payroll_field_targets_test.php, which resolves the
   cascade rather than grepping for "44px", and reads the markup side
   to prove no field has grown an inline height that would outrank
   this rule. */
@media (max-width: 640px) {
    .payroll-page input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]):not([type="image"]),
    .payroll-page select,
    .payroll-page textarea:not(.app-textarea),
    .payroll-page .app-btn {
        min-height: 44px;
    }
}

/* ============================================================
   Evidence side signal (.evidence-row)
   ============================================================
   Missing evidence is a side signal, not a wall (20.8.2026,
   inbox 20260820-183400): a work-queue row carries a slim left
   stripe and a short Finnish label chip. Never color alone —
   the label text is always present. States mirror the reader's
   own signal words: ok · review · missing. */
.evidence-row {
    border-left: 3px solid var(--line);
    padding: 4px 0 4px 10px;
    margin: 0 0 8px;
}

.evidence-row--ok      { border-left-color: #4d9a5d; }
.evidence-row--review  { border-left-color: #c67c00; }
.evidence-row--missing { border-left-color: #c9a227; }

.evidence-row__label {
    display: inline-block;
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    padding: 1px 7px;
    border-radius: 999px;
    background: var(--accent-soft, var(--cream, #f0f3ff));
    color: var(--ink);
    margin-bottom: 2px;
}

/* Raportti ennen valitsinta puhelimella (accounting.php, 21.8.2026)
   ---------------------------------------------------------------
   Below 640px the accounting content column becomes a flex column so the
   report picker can be ordered AFTER the content. Everything else keeps
   document order, because only the picker is given an order value.

   WHY ORDER AND NOT A FOLD: folding the picker would change what a visible
   control does, and the owner has not been asked. Ordering changes only
   where it sits. Desktop is deliberately outside this query -- the
   explanation measured at y=499 against a 950 fold there, so there is
   nothing to fix and a reorder would only make the two widths disagree
   for no reason.

   The markup half is `app-acc-col` / `app-acc-picker` in
   app/pages/accounting.php; tests/gl_tyopoyta_test.php reads the pair as
   ONE relation, so neither half can be removed on its own. */
@media (max-width: 640px) {
    .app-acc-col {
        display: flex;
        flex-direction: column;
    }

    .app-acc-col > .app-acc-picker {
        order: 2;
    }
}

/* Raportin laajuus (accounting.php, 22.8.2026)
   -------------------------------------------
   One muted line under a report's own heading, saying which span the report
   covers. Small and quiet on purpose: it is metadata a reader checks, not a
   warning. Readable at both widths without a media query -- there is nothing
   width-dependent about a single short line. */
.app-report-scope {
    margin: -4px 0 10px;
    color: var(--muted);
    font-size: 12px;
}

/* ============================================================
   PUUTTEELLISEN RAPORTIN VAROITUS (4.9.2026)
   ============================================================
   The notice above an ALV-erittely or a palkkayhteenveto that is
   showing figures which are NOT all of the books. Two paragraphs
   stack inside it: `--partial` says the numbers on screen are
   incomplete, and the plain one says the missing work is not lost.

   WHY IT IS HERE AND NOT IN THE MARKUP. Both paragraphs carried
   these five declarations as an INLINE style written by PHP.
   870ad21 added two of them on the same day 495d61b removed eleven
   from this very file, and the two changes were each correct
   alone — the direction monitor is what noticed the combination.
   An inline declaration is unreachable by every stylesheet, so
   while it stood, no rule here could have sized this warning at
   any width.

   WHY 16px, AT EVERY WIDTH. Not the iOS zoom rule: iOS Safari
   zooms when a FOCUSED text-entry control is under 16px, and a
   paragraph is never focused, so 13px here never zoomed anything.
   The reason is plainer. `body` is 14px, so this warning was
   rendering SMALLER than the ordinary text around it — and it is
   the sentence that stops a reader carrying a wrong figure onto
   an ALV- or työnantajailmoitus. A warning that must outrank its
   surroundings should not be set below them.

   ONE SIZE, NO BREAKPOINT, ON PURPOSE. The two paragraphs are one
   visual block; a max-width query that grew one and not the other
   would split a two-sentence warning into two sizes on a phone.
   It also keeps this file free of a second `@media (max-width:
   640px)` block, which is the ordering hazard the queue fix at
   line ~2937 had to document.

   NO IMPORTANCE OVERRIDE, AND NONE IS NEEDED: nothing else in this
   file selects a `p` inside `.app-card`, so a single class wins on
   its own. This stylesheet's override count is unchanged — raising
   it would be losing the arms race this class exists to end.

   AND THAT SENTENCE IS SPELT THAT WAY ON PURPOSE. The direction
   monitor counts the override keyword by plain text match, so it
   counts the ones in prose too: this comment naming the keyword
   was itself enough to move the ratchet 28 -> 29 and refuse the
   commit. Five of the 28 it has been guarding are comment mentions,
   not declarations. Reported rather than fixed here — narrowing
   that rule is the monitor's slice, not this one, and a check
   loosened by whoever it just caught is worth less than the noise
   it makes.
   ============================================================ */
.app-report-gap {
    font-size: 16px;
    margin: 0 0 12px;
    border-left: 3px solid var(--warning, #c67c00);
    padding-left: 10px;
}

/* The incompleteness sentence leads, so it carries the weight. It
   is the only difference between the two paragraphs; everything
   else they share stays in the base class rather than being
   repeated. */
.app-report-gap--partial {
    font-weight: 600;
}

/* Muistiotositteen elävä täsmäys (23.8.2026)
   -----------------------------------------
   Two states, and neither is a verdict: the server decides whether a voucher
   posts. `--off` is muted rather than alarming because an unfinished voucher
   is the normal state while typing, not an error. Colour is never the only
   signal -- the difference is spelled out in the text beside it. */
.mv-balance { color: var(--muted); }
.mv-balance--even { color: var(--ink); font-weight: 600; }
.mv-balance--off  { color: var(--ink); }

/* Sama täsmäys myös rivien yläpuolella, vain puhelimessa (3.9.2026)
   ----------------------------------------------------------------
   Measured at 375x812: the readout below the table is 3 125px — 3.85
   screenfuls — under the row-1 amount field that drives it, so a person could
   not see the running debet/kredit while typing the amounts. The page
   therefore renders a SECOND readout above the rows; the one below the table
   is untouched and still stands immediately above the button.

   Above 640px the table and the readout below it share one screen, so a
   second identical totals line would be noise — hence display:none there.
   This hides nothing that existed before: the element is new, and the
   canonical `#mv-balance` is visible at every width.

   THE TWO RULES BELOW MUST STAY ADJACENT AND IN THIS ORDER. Both are
   specificity (0,1,0), so the later one wins; the mobile override cannot be
   moved up into the .mv-lines @media block at line ~2650, which appears
   EARLIER in this file and would therefore lose to the display:none above
   it — the fix would render nothing and every "the element exists" check
   would still pass. */
.mv-balance--top { display: none; }

@media (max-width: 640px) {
    .mv-balance--top {
        display: block;
        font-size: 13px;
        margin: 0 0 10px;
    }
}

/* Euroapuri (23.8.2026) — HIDDEN UNTIL THE SCRIPT THAT SERVES IT EXISTS.
   The helper carries no name, so it is never posted. That makes it safe from
   the server's side and UNSAFE from the reader's if it is visible without the
   script: a person could type 12,40 into a box that looks like a field, and
   the amount would go nowhere. Review caught exactly that, and my own no-JS
   check had asserted "present but inert" as though inert were the same as
   harmless. Display:none is not focusable either, so without JavaScript the
   helper cannot be reached at all and the cents field is simply the field. */
.mv-euro { display: none; }
.mv-euro--ready { display: inline-block; }

/* T018W-C Slice 3.1A correction 01 — access page, narrow widths only.
   Scoped to the roles-and-access custom-role form: at 390 px the four-column
   grid (Pohja / Nimi / Tunnus / button) squeezed every field until labels,
   selected values and placeholders were clipped. One column per field is
   readable and operable; desktop is untouched because the rule only applies
   below 640 px. Deliberately NOT a change to the shared admin mobile table. */
@media (max-width: 640px) {
    .access-clone-grid {
        grid-template-columns: 1fr !important;
        align-items: stretch !important;
    }

    .access-clone-grid .app-input,
    .access-clone-grid .app-btn {
        width: 100%;
        box-sizing: border-box;
    }
}

/* T018W-F1 correction 1: the ordinary role editor's policy grid.
   At 390px three columns compute to ~76-88px each, which turns a business
   editor into three unreadable strips. Narrowly scoped to this one grid so no
   shared admin-table behaviour changes. */
.role-builder-section {
    padding: 18px 0;
    border-top: 1px solid var(--line);
}
.role-builder-section:first-of-type { border-top: 0; padding-top: 4px; }
.role-builder-section h4 { margin: 0 0 12px; font-size: 15px; letter-spacing: 0; }
.role-builder-note { margin: -6px 0 12px; color: var(--muted); font-size: 12px; }
.role-scope-grid,
.role-access-options {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 8px;
}
.role-choice-card,
.role-area-card__toggle {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    cursor: pointer;
}
.role-choice-card {
    min-height: 66px;
    padding: 12px;
    border: 1px solid var(--line);
    border-radius: 6px;
    background: var(--surface);
    box-sizing: border-box;
}
.role-choice-card:has(input:checked) { border-color: var(--accent); background: var(--accent-soft); }
.role-choice-card strong,
.role-choice-card small,
.role-area-card__toggle strong,
.role-area-card__toggle small { display: block; letter-spacing: 0; }
.role-choice-card small,
.role-area-card__toggle small { margin-top: 3px; color: var(--muted); font-size: 12px; line-height: 1.35; }
.role-policy-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 10px;
}
.role-area-card {
    min-width: 0;
    border: 1px solid var(--line);
    border-radius: 6px;
    background: var(--surface);
}
.role-area-card.is-selected { border-color: var(--accent); }
.role-area-card h5 {
    margin: 0;
    padding: 14px 14px 10px;
    font-size: 14px;
    letter-spacing: 0;
}
.role-schema-operations {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 1px;
    margin: 0 14px 14px;
    border: 1px solid var(--line);
    border-radius: 6px;
    overflow: hidden;
    background: var(--line);
}
.role-schema-operations label {
    display: block;
    min-width: 0;
    cursor: pointer;
    background: var(--surface);
}
.role-schema-operations input { position: absolute; opacity: 0; pointer-events: none; }
.role-schema-operations span {
    display: block;
    padding: 9px 5px;
    text-align: center;
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0;
}
.role-schema-operations input:checked + span {
    color: var(--accent-ink, var(--accent));
    background: var(--accent-soft);
}
.role-area-card__toggle { min-height: 72px; padding: 14px; box-sizing: border-box; }
.role-area-card__toggle .material-symbols-rounded { color: var(--muted); font-size: 22px; }
.role-area-card.is-selected .role-area-card__toggle .material-symbols-rounded { color: var(--accent); }

/* Paged, searchable member list on the access page. */
.access-member-filter {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    align-items: center;
    margin: 0 0 12px;
}
.access-member-filter .app-input { width: auto; min-width: 0; }
.access-member-filter input[type="search"] { flex: 1 1 220px; }
.access-member-count { margin-left: auto; font-size: 12px; color: var(--muted); }
.access-member-pager {
    display: flex;
    gap: 10px;
    align-items: center;
    justify-content: flex-end;
    margin-top: 12px;
    font-size: 13px;
    color: var(--muted);
}

@media (max-width: 640px) {
    /* At 390px the count belongs under the controls, not squeezed beside them. */
    .access-member-count { margin-left: 0; width: 100%; }
    .access-member-pager { justify-content: space-between; }
}

/* T018W-F2.6: product grouping, and the schema as the parent control. */
.role-schema-group + .role-schema-group { margin-top: 18px; }
.role-schema-group__title {
    margin: 0 0 8px;
    font-size: 12px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--muted);
}
.role-area-card__head {
    display: flex;
    align-items: baseline;
    gap: 8px;
    min-width: 0;                    /* lets the long state text shrink, not push */
    padding: 12px 14px;
}
/* Overrides the generic .role-area-card__toggle box above: here the toggle is a
   compact inline control in the card header, not the whole card face. */
.role-area-card__head .role-area-card__toggle {
    display: flex;
    align-items: baseline;
    gap: 8px;
    min-height: 0;
    min-width: 0;
    padding: 0;
    cursor: pointer;
}
.role-area-card__head .role-area-card__toggle input { flex: none; cursor: pointer; }
.role-area-card__head .role-area-card__toggle input:disabled { cursor: not-allowed; }
.role-area-card__name {
    font-size: 14px;
    font-weight: 600;
    letter-spacing: 0;
    overflow-wrap: anywhere;
}
.role-area-card__state {
    margin-left: auto;
    font-size: 12px;
    color: var(--muted);
    text-align: right;
    overflow-wrap: anywhere;         /* wraps rather than widening the grid */
}
.role-area-card.is-selected .role-area-card__state { color: var(--accent-ink, var(--accent)); }
.role-area-card__body[hidden] { display: none; }

/* Grouped rights in the two detail disclosures, matching the editor. */
.role-schema-details__group { min-width: 0; }
.role-schema-details__title {
    margin: 0 0 4px;
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--muted);
}
.role-area-workflows { border-top: 1px solid var(--line); padding: 0 14px 12px; }
.role-area-workflows summary,
.role-shared-workflows > summary { padding: 10px 0 0; cursor: pointer; font-size: 12px; font-weight: 600; }
.role-workflow-group { margin-top: 12px; }
.role-workflow-group > strong { display: block; margin-bottom: 4px; font-size: 12px; }
.role-workflow-group label { display: block; padding: 3px 0; font-size: 13px; }
.role-shared-workflows { margin: 0; }
.role-dependency-message {
    margin-top: 12px;
    padding: 10px 12px;
    border-left: 3px solid var(--warning);
    background: var(--bg);
    color: var(--ink);
    font-size: 13px;
}
.role-schema-details {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 8px 16px;
    padding: 10px 12px;
    font-size: 13px;
}
.role-schema-details > div { min-width: 0; }
.role-schema-details strong,
.role-schema-details span,
.role-schema-details small { display: block; }
.role-schema-details span,
.role-schema-details small {
    color: var(--muted);
    overflow-wrap: anywhere;
}
.role-schema-details small { margin-top: 2px; }

@media (max-width: 640px) {
    .role-policy-grid {
        grid-template-columns: 1fr !important;
        gap: 10px !important;
    }
    .role-scope-grid,
    .role-access-options { grid-template-columns: 1fr; }
    .role-schema-details { grid-template-columns: 1fr; }
    .role-choice-card { min-height: 58px; }
    /* T018W-F2.6: at 390px the name and its state stack instead of competing
       for one line, which is what produced horizontal overflow. */
    .role-area-card__head { flex-wrap: wrap; }
    .role-area-card__state { margin-left: 0; width: 100%; text-align: left; }
}

/* Work-order dispatch workload board. The weekly matrix owns exactly one
   horizontal scroller; the surrounding page and dialog remain contained. */
.dispatch-workload { min-width: 0; max-width: 100%; }
.dispatch-workload__heading {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: 16px;
    margin-bottom: 10px;
}
.dispatch-workload__heading h2 { margin: 0; font-size: 18px; }
.dispatch-workload__heading p { margin: 4px 0 0; color: var(--muted); font-size: 13px; }
.dispatch-workload__nav { display: flex; align-items: center; gap: 8px; margin: 0; }
.dispatch-workload__nav .cal-nav-title { font-weight: 600; white-space: nowrap; }
.dispatch-status {
    min-height: 0;
    margin: 0 0 10px;
    padding: 0;
    border-radius: 6px;
    font-size: 13px;
}
.dispatch-status:not(:empty) { min-height: 20px; padding: 8px 10px; }
.dispatch-status.is-success { color: #166534; background: rgba(34, 197, 94, .12); }
.dispatch-status.is-error { color: #991b1b; background: rgba(239, 68, 68, .12); }
.dispatch-board-wrap {
    width: 100%;
    max-width: 100%;
    min-width: 0;
    overflow-x: auto;
    overscroll-behavior-inline: contain;
    border: 1px solid var(--line);
    border-radius: 8px;
    background: var(--surface);
    box-sizing: border-box;
}
.dispatch-board {
    width: 100%;
    min-width: 1120px;
    border-collapse: separate;
    border-spacing: 0;
    table-layout: fixed;
    font-size: 12px;
}
.dispatch-board th,
.dispatch-board td { border: 0; border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); }
.dispatch-board tr > :last-child { border-right: 0; }
.dispatch-board tfoot tr:last-child > * { border-bottom: 0; }
.dispatch-board__worker-head,
.dispatch-board__worker { width: 132px; text-align: left; }
.dispatch-board__day-head { width: 122px; text-align: left; }
.dispatch-board__day-head span { display: block; margin-top: 2px; color: var(--muted); font-size: 11px; font-weight: 400; }
.dispatch-board__total-head,
.dispatch-board__worker-total { width: 92px; text-align: right; }
.dispatch-board__worker {
    padding: 10px;
    vertical-align: top;
    background: var(--surface);
    font-weight: 600;
}
.dispatch-board__worker span,
.dispatch-board__worker small { display: block; }
.dispatch-board__worker small { margin-top: 4px; color: var(--muted); font-size: 11px; font-weight: 400; }
.dispatch-board__worker.is-muted { color: var(--muted); font-weight: 500; }
.dispatch-board__worker-total { padding: 10px 8px; vertical-align: top; }
.dispatch-board__worker-total strong,
.dispatch-board__worker-total span { display: block; }
.dispatch-board__worker-total span { margin-top: 3px; color: var(--muted); font-size: 11px; }
.dispatch-cell { padding: 0; vertical-align: top; transition: background-color .15s, box-shadow .15s; }
.dispatch-cell.is-today:not(.dispatch-load--ok):not(.dispatch-load--near):not(.dispatch-load--over) { background: rgba(59, 130, 246, .05); }
.dispatch-cell.dispatch-load--ok { background: rgba(16, 185, 129, .11); }
.dispatch-cell.dispatch-load--near { background: rgba(245, 158, 11, .14); }
.dispatch-cell.dispatch-load--over { background: rgba(239, 68, 68, .14); }
.dispatch-cell.is-dragover { box-shadow: inset 0 0 0 2px var(--accent); background: var(--accent-soft); }
.dispatch-cell__load {
    display: flex;
    gap: 3px;
    min-height: 28px;
    padding: 7px 7px 5px;
    color: var(--muted);
    font-size: 11px;
    font-weight: 600;
}
.dispatch-queue { min-height: 74px; padding: 0 5px 7px; }
.dispatch-card {
    position: relative;
    min-width: 0;
    margin-top: 5px;
    padding: 8px;
    border: 1px solid var(--line);
    border-radius: 6px;
    background: var(--surface);
    box-shadow: 0 1px 2px rgba(15, 23, 42, .06);
}
.dispatch-card[draggable="true"] { cursor: grab; touch-action: none; }
.dispatch-card[draggable="true"]:active { cursor: grabbing; }
.dispatch-card.is-dragging { opacity: .45; }
.dispatch-card.is-busy { opacity: .65; }
.dispatch-card.is-locked { border-style: dashed; }
.dispatch-card__title {
    display: block;
    color: var(--ink);
    font-size: 12px;
    font-weight: 650;
    line-height: 1.35;
    overflow-wrap: anywhere;
    text-decoration: none;
}
.dispatch-card__title:hover { color: var(--accent); text-decoration: underline; }
.dispatch-card__meta { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 6px; margin-top: 6px; color: var(--muted); font-size: 10px; }
.dispatch-card__meta .app-badge { max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
.dispatch-card__move {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    margin-top: 7px;
    padding: 3px 5px;
    border: 0;
    border-radius: 4px;
    color: var(--accent);
    background: transparent;
    font: inherit;
    font-weight: 600;
    cursor: pointer;
}
.dispatch-card__move:hover,
.dispatch-card__move:focus-visible { background: var(--accent-soft); }
.dispatch-card__move:disabled { color: var(--muted); cursor: not-allowed; opacity: .65; }
.dispatch-card__move .material-symbols-rounded { font-size: 16px; }
.dispatch-board tfoot th,
.dispatch-board tfoot td { padding: 8px; color: var(--muted); text-align: right; font-weight: 650; }
.dispatch-board tfoot th { text-align: left; color: var(--ink); }
.dispatch-legend { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; margin-top: 10px; color: var(--muted); font-size: 12px; }
.dispatch-legend span { display: inline-flex; align-items: center; gap: 5px; }
.dispatch-legend i { width: 11px; height: 11px; border-radius: 3px; }
.dispatch-legend i.dispatch-load--ok { background: rgba(16, 185, 129, .35); }
.dispatch-legend i.dispatch-load--near { background: rgba(245, 158, 11, .45); }
.dispatch-legend i.dispatch-load--over { background: rgba(239, 68, 68, .45); }
.dispatch-dialog {
    width: min(420px, calc(100vw - 24px));
    max-width: calc(100vw - 24px);
    padding: 0;
    border: 1px solid var(--line);
    border-radius: 10px;
    color: var(--ink);
    background: var(--surface);
    box-shadow: 0 20px 60px rgba(15, 23, 42, .28);
    box-sizing: border-box;
}
.dispatch-dialog::backdrop { background: rgba(15, 23, 42, .42); }
.dispatch-dialog form { display: grid; gap: 14px; padding: 18px; }
.dispatch-dialog__head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.dispatch-dialog__head h3 { margin: 0; font-size: 18px; }
.dispatch-dialog__close { padding: 2px 7px; border: 0; color: var(--muted); background: transparent; font-size: 24px; cursor: pointer; }
.dispatch-dialog__job { margin: -6px 0 0; color: var(--muted); overflow-wrap: anywhere; }
.dispatch-dialog label > span { display: block; margin-bottom: 5px; font-size: 13px; font-weight: 600; }
.dispatch-dialog .app-input { width: 100%; box-sizing: border-box; }
.dispatch-dialog__actions { display: flex; justify-content: flex-end; gap: 8px; }

@media (max-width: 640px) {
    .dispatch-workload__heading { align-items: flex-start; flex-direction: column; gap: 10px; }
    .dispatch-workload__nav { width: 100%; flex-wrap: wrap; }
    .dispatch-board-wrap { max-width: 100%; }
    .dispatch-card__move { min-height: 36px; padding: 6px 7px; }
    .dispatch-dialog form { padding: 16px; }
}

/* Mobile worker day: one focused dashboard job and one above-the-fold action
   band on the existing work-order page. The same DOM stays compact on desktop. */
.worker-focus { margin-bottom: 16px; border-left: 3px solid var(--accent); }
.worker-focus__header { gap: 12px; }
.worker-focus__body {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    min-width: 0;
}
.worker-focus__summary { min-width: 0; }
.worker-focus__title { font-size: 18px; font-weight: 700; overflow-wrap: anywhere; }
.worker-focus__meta { display: flex; flex-wrap: wrap; gap: 6px 10px; align-items: center; margin-top: 7px; color: var(--muted); font-size: 13px; }
.worker-focus__open { flex: 0 0 auto; min-height: 44px; justify-content: center; }
/* The home-screen hour log. Reuses .worker-day-time-form's grid, its 44px
   buttons and its <=640px reflow verbatim; only the separation from the job
   summary above is new here. */
.worker-focus__alert { margin: 12px 0 0; }
.worker-focus__time { border-top: 1px solid var(--line); padding-top: 12px; }

.worker-day-band {
    min-width: 0;
    margin: 0 0 16px;
    padding: 14px 16px;
    border: 1px solid var(--line);
    border-left: 3px solid var(--accent);
    border-radius: var(--radius);
    background: var(--surface);
    box-sizing: border-box;
}
.worker-day-band__heading h2 { margin: 0; font-size: 17px; }
.worker-day-band__heading p { margin: 3px 0 0; color: var(--muted); font-size: 13px; }
.worker-day-band__alert { margin: 10px 0 0; }
.worker-day-band__primary-form,
.worker-day-band__complete-form { margin: 12px 0 0; }
.worker-day-band__primary,
.worker-day-band__complete { min-height: 44px; justify-content: center; }
.worker-day-time-form {
    display: grid;
    grid-template-columns: minmax(130px, .7fr) minmax(84px, .45fr) minmax(220px, 1.8fr) auto;
    gap: 10px;
    align-items: end;
    min-width: 0;
    margin-top: 12px;
}
.worker-day-time-form label { min-width: 0; }
.worker-day-time-form label > span { display: block; margin-bottom: 4px; color: var(--muted); font-size: 12px; font-weight: 600; }
.worker-day-time-form .app-input { width: 100%; min-width: 0; box-sizing: border-box; }
.worker-day-time-form__buttons { display: flex; flex-wrap: wrap; gap: 8px; }
.worker-day-time-form__buttons .app-btn { min-height: 44px; justify-content: center; }
.worker-day-band__secondary { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-top: 12px; }
.worker-day-band__secondary form { margin: 0; }
.worker-day-band__secondary .app-btn { min-height: 44px; max-width: 100%; white-space: normal; text-align: left; }
.worker-day-band__secondary .material-symbols-rounded { font-size: 18px; }
.worker-day-band__unavailable {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding: 0 10px;
    border: 1px dashed var(--line);
    border-radius: var(--radius-sm);
    color: var(--muted);
    font-size: 12px;
    overflow-wrap: anywhere;
}

.worker-material-form {
    display: grid;
    grid-template-columns: minmax(180px, 1.8fr) minmax(88px, .55fr) minmax(100px, .65fr) minmax(170px, 1fr);
    gap: 10px;
    min-width: 0;
    margin-top: 14px;
    padding-top: 14px;
    border-top: 1px solid var(--line);
}
.worker-material-form[hidden] { display: none; }
.worker-material-form label { min-width: 0; }
.worker-material-form label > span { display: block; margin-bottom: 4px; color: var(--muted); font-size: 12px; font-weight: 600; }
.worker-material-form .app-input { width: 100%; min-width: 0; box-sizing: border-box; }
.worker-material-form__header { grid-column: 1 / -1; display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.worker-material-form__header h3 { margin: 0; font-size: 15px; }
.worker-material-form__catalog { grid-column: 1 / -1; }
.worker-material-form__catalog [data-catalog-pick] { width: min(100%, 420px); }
.worker-material-form__catalog .catalog-pick-input { width: 100%; min-height: 44px; box-sizing: border-box; }
.worker-material-form__buttons { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 8px; }
.worker-material-form__buttons .app-btn { min-height: 44px; }
.worker-materials { min-width: 0; margin: 0 0 16px; padding: 14px 16px; border: 1px solid var(--line); border-radius: 6px; background: var(--surface); box-sizing: border-box; }
.worker-materials__heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.worker-materials__heading h2 { margin: 0; font-size: 17px; }
.worker-materials__heading p, .worker-materials__empty { margin: 3px 0 0; color: var(--muted); font-size: 13px; }
.worker-materials__list { display: grid; gap: 0; margin-top: 10px; }
.worker-material-row { position: relative; display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px 14px; min-width: 0; padding: 10px 0; border-top: 1px solid var(--line); }
.worker-material-row:first-child { border-top: 0; }
.worker-material-row__main { display: flex; flex-wrap: wrap; gap: 5px 10px; min-width: 0; align-items: baseline; }
.worker-material-row__main strong { min-width: 0; overflow-wrap: anywhere; }
.worker-material-row__main span { color: var(--muted); white-space: nowrap; }
.worker-material-row__meta { grid-column: 1; display: flex; flex-wrap: wrap; gap: 4px 12px; color: var(--muted); font-size: 12px; }
.worker-material-correction { grid-column: 2; grid-row: 1 / span 2; align-self: start; }
.worker-material-correction > summary { list-style: none; min-width: 44px; min-height: 44px; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; }
.worker-material-correction > summary::-webkit-details-marker { display: none; }
.worker-material-correction[open] { grid-column: 1 / -1; grid-row: auto; width: 100%; min-width: 0; }
.worker-material-correction[open] > summary { display: flex; margin-left: auto; }
.worker-material-form--correction { grid-column: 1 / -1; grid-template-columns: minmax(180px, 1.7fr) minmax(80px, .5fr) minmax(96px, .6fr) minmax(150px, 1fr); margin: 10px 0 0; }

@media (max-width: 640px) {
    .worker-focus__header { align-items: flex-start; }
    .worker-focus__body { align-items: stretch; flex-direction: column; gap: 12px; }
    .worker-focus__open { width: 100%; box-sizing: border-box; }
    .worker-day-band { padding: 14px; }
    .worker-day-band__primary-form,
    .worker-day-band__complete-form,
    .worker-day-band__primary,
    .worker-day-band__complete { width: 100%; box-sizing: border-box; }
    .worker-day-time-form { grid-template-columns: minmax(0, 1.35fr) minmax(84px, .65fr); }
    .worker-day-time-form__description,
    .worker-day-time-form__buttons { grid-column: 1 / -1; }
    .worker-day-time-form__buttons .app-btn { flex: 1 1 140px; }
    .worker-day-band__secondary { align-items: stretch; flex-direction: column; }
    .worker-day-band__secondary form,
    .worker-day-band__secondary .app-btn,
    .worker-day-band__unavailable { width: 100%; box-sizing: border-box; justify-content: center; text-align: center; }
    .worker-material-form,
    .worker-material-form--correction { grid-template-columns: minmax(0, 1fr) minmax(104px, .72fr); }
    .worker-material-form__header,
    .worker-material-form__catalog,
    .worker-material-form__description,
    .worker-material-form__buttons { grid-column: 1 / -1; }
    .worker-material-form__buttons .app-btn { flex: 1 1 130px; justify-content: center; }
    .worker-materials { padding: 14px; }
    .worker-material-row { grid-template-columns: minmax(0, 1fr) 44px; }
}

/* Project billing review: one operational surface for accepted scope,
   completed sources and earlier billing. It freezes evidence only; invoice
   creation remains in the existing card below it. */
.progress-billing { margin-top: 16px; min-width: 0; }
.progress-billing__header { align-items: flex-start; gap: 12px; }
.progress-billing__header h2 { margin: 0; font-size: 18px; }
.progress-billing__header p { margin: 4px 0 0; color: var(--muted); font-size: 13px; }
.progress-billing__alert { margin: 0 0 14px; }
.progress-billing__summary {
    display: grid;
    grid-template-columns: repeat(5, minmax(112px, 1fr));
    gap: 1px;
    overflow: hidden;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--line);
}
.progress-billing__summary > div { min-width: 0; padding: 11px 12px; background: var(--surface); }
.progress-billing__summary span { display: block; min-height: 30px; color: var(--muted); font-size: 11px; font-weight: 600; }
.progress-billing__summary strong { display: block; margin-top: 4px; font-size: 16px; overflow-wrap: anywhere; }
.progress-billing__summary .progress-billing__summary-primary { background: var(--accent-soft); }
.progress-billing__issues { margin-top: 12px; padding: 10px 12px; border-left: 3px solid var(--warning); background: var(--canvas); font-size: 13px; }
.progress-billing__issues ul { margin: 5px 0 0; padding-left: 18px; color: var(--muted); }
.progress-billing__columns { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px; margin-top: 18px; }
.progress-billing__columns--details { margin-top: 12px; }
.progress-billing__group { min-width: 0; }
.progress-billing__group h3 { margin: 0 0 7px; font-size: 14px; }
.progress-billing__provenance { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 6px 10px; margin-bottom: 5px; color: var(--muted); font-size: 11px; }
.progress-billing__provenance code { font-size: 11px; }
.progress-billing__rows { border-top: 1px solid var(--line); }
.progress-billing__row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; min-width: 0; padding: 8px 0; border-bottom: 1px solid var(--line); font-size: 12px; }
.progress-billing__row > :first-child { min-width: 0; overflow-wrap: anywhere; }
.progress-billing__row > :last-child { text-align: right; }
.progress-billing__row a { color: var(--accent); font-weight: 600; }
.progress-billing__empty { margin: 8px 0; color: var(--muted); font-size: 12px; }
.progress-billing__details { margin-top: 14px; border-top: 1px solid var(--line); }
.progress-billing__details > summary { min-height: 44px; display: flex; align-items: center; color: var(--accent); font-size: 13px; font-weight: 650; cursor: pointer; }
.progress-billing__prior { margin-top: 16px; }
.progress-billing__footer { display: flex; align-items: flex-end; justify-content: space-between; gap: 20px; margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--line); }
.progress-billing__footer p { margin: 3px 0 0; color: var(--muted); font-size: 12px; }
.progress-billing__confirm { display: flex; align-items: center; justify-content: flex-end; flex-wrap: wrap; gap: 10px 14px; }
.progress-billing__confirm label { display: inline-flex; align-items: flex-start; gap: 7px; max-width: 300px; font-size: 12px; }
.progress-billing__confirm input[type="checkbox"] { flex: 0 0 auto; margin-top: 2px; }
.progress-billing__confirm .app-btn { min-height: 44px; }
.progress-billing__readonly { display: inline-flex; align-items: center; min-height: 36px; padding: 0 10px; border: 1px solid var(--line); border-radius: var(--radius-sm); color: var(--muted); font-size: 12px; }

/* Recurring customer-site maintenance planner. */
.maintenance-page { min-width: 0; }
.maintenance-page__header { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; margin-bottom: 18px; }
.maintenance-page__header h1 { margin: 0; font-size: 22px; }
.maintenance-page__header p { max-width: 680px; margin: 5px 0 0; color: var(--muted); font-size: 13px; }
.maintenance-page__actions { display: flex; align-items: flex-end; justify-content: flex-end; flex-wrap: wrap; gap: 10px; }
.maintenance-page__date { display: flex; align-items: center; gap: 8px; }
.maintenance-page__date label { color: var(--muted); font-size: 12px; font-weight: 600; }
.maintenance-page__date input { min-height: 38px; box-sizing: border-box; border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--paper); color: var(--ink); padding: 6px 9px; }
.maintenance-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); border: 1px solid var(--line); background: var(--paper); margin-bottom: 16px; }
.maintenance-summary > div { min-width: 0; padding: 12px 14px; border-right: 1px solid var(--line); }
.maintenance-summary > div:last-child { border-right: 0; }
.maintenance-summary span { display: block; color: var(--muted); font-size: 11px; font-weight: 600; }
.maintenance-summary strong { display: block; margin-top: 3px; font-size: 19px; }
.maintenance-list { display: grid; gap: 10px; }
.maintenance-item { display: grid; grid-template-columns: minmax(0, 1fr) minmax(190px, 250px); gap: 18px; align-items: center; min-width: 0; padding: 16px 18px; border: 1px solid var(--line); border-left: 3px solid var(--accent); background: var(--paper); }
.maintenance-item--overdue { border-left-color: var(--danger); }
.maintenance-item__main { min-width: 0; }
.maintenance-item__heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.maintenance-item__heading h2 { margin: 0; font-size: 16px; }
.maintenance-item__heading h2 a { color: var(--ink); text-decoration: none; }
.maintenance-item__heading p { margin: 3px 0 0; color: var(--muted); font-size: 12px; overflow-wrap: anywhere; }
.maintenance-item__facts { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; margin: 13px 0 0; }
.maintenance-item__facts div { min-width: 0; }
.maintenance-item__facts dt { color: var(--muted); font-size: 10px; font-weight: 600; text-transform: uppercase; }
.maintenance-item__facts dd { margin: 3px 0 0; font-size: 12px; overflow-wrap: anywhere; }
.maintenance-item__issue { display: flex; align-items: center; gap: 6px; margin: 12px 0 0; color: var(--warning); font-size: 12px; }
.maintenance-item__issue .material-symbols-rounded { font-size: 17px; }
.maintenance-item__action { min-width: 0; }
.maintenance-confirm { display: grid; gap: 10px; }
.maintenance-confirm label { display: flex; align-items: flex-start; gap: 7px; font-size: 12px; }
.maintenance-confirm input[type="checkbox"] { flex: 0 0 auto; margin-top: 2px; }
.maintenance-confirm .app-btn { width: 100%; min-height: 44px; box-sizing: border-box; justify-content: center; }
.maintenance-empty { border: 1px solid var(--line); background: var(--paper); }
.work-order-maintenance-origin { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
.work-order-maintenance-origin .material-symbols-rounded { font-size: 18px; }
.work-order-maintenance-origin a { margin-left: auto; font-weight: 600; }

/* Customer site sign-off: one mobile-first evidence journey. */
.work-order-signoff-band { display: flex; align-items: center; justify-content: space-between; gap: 18px; margin: 16px 0; padding: 14px 16px; border: 1px solid var(--line); border-left: 3px solid var(--accent); background: var(--paper); }
.work-order-signoff-band h2 { margin: 2px 0 0; font-size: 15px; }
.work-order-signoff-band p { margin: 4px 0 0; color: var(--muted); font-size: 12px; }
.work-order-signoff-band__label { color: var(--muted); font-size: 10px; font-weight: 700; text-transform: uppercase; }
.site-signoff { width: min(100%, 820px); margin: 0 auto; min-width: 0; }
.site-signoff__back { margin-bottom: 14px; }
.site-signoff__back a { display: inline-flex; align-items: center; gap: 6px; color: var(--muted); font-size: 12px; text-decoration: none; }
.site-signoff__back .material-symbols-rounded { font-size: 18px; }
.site-signoff__header { display: flex; align-items: flex-start; justify-content: space-between; gap: 18px; margin-bottom: 18px; }
.site-signoff__header h1 { margin: 3px 0 0; font-size: 24px; }
.site-signoff__header p { max-width: 660px; margin: 7px 0 0; color: var(--muted); font-size: 13px; }
.site-signoff__eyebrow, .site-signoff__section-heading span { color: var(--muted); font-size: 10px; font-weight: 700; text-transform: uppercase; }
.site-signoff__work, .site-signoff__form-panel, .site-signoff__accepted, .site-signoff__history { margin-top: 14px; padding: 16px; border: 1px solid var(--line); background: var(--paper); }
.site-signoff__section-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; }
.site-signoff__section-heading h2 { margin: 3px 0 0; font-size: 17px; }
.site-signoff__facts { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin: 16px 0 0; }
.site-signoff__facts div { min-width: 0; }
.site-signoff__facts dt { color: var(--muted); font-size: 10px; font-weight: 700; text-transform: uppercase; }
.site-signoff__facts dd { margin: 3px 0 0; font-size: 13px; overflow-wrap: anywhere; }
.site-signoff__description { margin: 14px 0 0; padding-top: 14px; border-top: 1px solid var(--line); font-size: 13px; line-height: 1.55; }
.site-signoff__timeline { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); margin-top: 14px; border: 1px solid var(--line); background: var(--paper); }
.site-signoff__timeline-step { display: flex; gap: 10px; min-width: 0; padding: 14px 16px; color: var(--muted); }
.site-signoff__timeline-step + .site-signoff__timeline-step { border-left: 1px solid var(--line); }
.site-signoff__timeline-step--done { color: var(--ink); }
.site-signoff__timeline-step .material-symbols-rounded { flex: 0 0 auto; color: var(--accent); }
.site-signoff__timeline-step strong { font-size: 13px; }
.site-signoff__timeline-step p { margin: 3px 0 0; color: var(--muted); font-size: 11px; overflow-wrap: anywhere; }
.site-signoff-form { display: grid; gap: 14px; margin-top: 18px; }
.site-signoff-form__identity { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
.site-signoff-form__identity label:last-child { grid-column: 1 / -1; }
.site-signoff-form__field { display: grid; gap: 6px; min-width: 0; }
.site-signoff-form__field > span { font-size: 12px; font-weight: 600; }
.site-signoff-form textarea { resize: vertical; min-height: 82px; }
.site-signoff-form__confirm { display: flex; align-items: flex-start; gap: 9px; padding: 12px; border: 1px solid var(--accent); background: var(--accent-soft); font-size: 13px; line-height: 1.45; }
.site-signoff-form__confirm input { flex: 0 0 auto; margin-top: 2px; }
.site-signoff-form__submit { min-height: 48px; justify-content: center; }
.site-signoff-form__boundary { margin: -3px 0 0; color: var(--muted); font-size: 11px; text-align: center; }
.site-signoff__accepted { display: flex; gap: 13px; border-left: 3px solid var(--success); }
.site-signoff__accepted > .material-symbols-rounded { flex: 0 0 auto; color: var(--success); font-size: 28px; }
.site-signoff__accepted h2 { margin: 0; font-size: 17px; }
.site-signoff__accepted p { margin: 5px 0 0; color: var(--muted); font-size: 12px; }
.site-signoff__accepted blockquote { margin: 13px 0 0; padding: 12px; border-left: 2px solid var(--line-strong); background: var(--surface); font-size: 13px; }
.site-signoff__history-list { display: grid; gap: 8px; margin-top: 14px; }
.site-signoff__history-list article { padding: 12px; border: 1px solid var(--line); }
.site-signoff__history-list article > div { display: flex; justify-content: space-between; gap: 12px; }
.site-signoff__history-list span, .site-signoff__history-list code { color: var(--muted); font-size: 11px; }
.site-signoff__history-list p { margin: 7px 0; font-size: 12px; }

@media (max-width: 900px) {
    .progress-billing__summary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .progress-billing__summary-primary { grid-column: 1 / -1; }
}

@media (max-width: 640px) {
    .progress-billing__header { flex-direction: column; }
    .progress-billing__summary { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
    .progress-billing__summary > div { padding: 10px; }
    .progress-billing__summary span { min-height: 44px; }
    .progress-billing__summary strong { font-size: 15px; }
    .progress-billing__columns { grid-template-columns: minmax(0, 1fr); gap: 16px; }
    .progress-billing__footer { align-items: stretch; flex-direction: column; }
    .progress-billing__confirm { align-items: stretch; flex-direction: column; }
    .progress-billing__confirm label { max-width: none; }
    .progress-billing__confirm .app-btn,
    .progress-billing__readonly { width: 100%; box-sizing: border-box; justify-content: center; }
    .maintenance-page__header { flex-direction: column; }
    .maintenance-page__actions { width: 100%; justify-content: flex-start; }
    .maintenance-page__date { width: 100%; display: grid; grid-template-columns: minmax(0, 1fr) auto; }
    .maintenance-page__date label { grid-column: 1 / -1; }
    .maintenance-page__actions > .app-btn { width: 100%; box-sizing: border-box; justify-content: center; }
    .maintenance-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .maintenance-summary > div:nth-child(2) { border-right: 0; }
    .maintenance-summary > div:nth-child(-n+2) { border-bottom: 1px solid var(--line); }
    .maintenance-item { grid-template-columns: minmax(0, 1fr); gap: 14px; padding: 14px; }
    .maintenance-item__facts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .maintenance-item__action > .app-btn { width: 100%; box-sizing: border-box; justify-content: center; }
    .work-order-maintenance-origin a { width: 100%; margin-left: 26px; }
    .work-order-signoff-band { align-items: stretch; flex-direction: column; }
    .work-order-signoff-band .app-btn { width: 100%; box-sizing: border-box; justify-content: center; min-height: 44px; }
    .site-signoff__header { flex-direction: column; }
    .site-signoff__facts, .site-signoff__timeline, .site-signoff-form__identity { grid-template-columns: minmax(0, 1fr); }
    .site-signoff__timeline-step + .site-signoff__timeline-step { border-left: 0; border-top: 1px solid var(--line); }
    .site-signoff-form__identity label:last-child { grid-column: auto; }
    .site-signoff__history-list article > div { flex-direction: column; gap: 3px; }
}

/* Owner/office pipeline report: dense, explainable and read-only. */
.pipeline-report { min-width: 0; }
.pipeline-report__header { display: flex; align-items: flex-end; justify-content: space-between; gap: 18px; }
.pipeline-report__header h1 { margin: 0; font-size: 24px; }
.pipeline-report__header p { margin: 5px 0 0; color: var(--muted); font-size: 13px; }
.pipeline-report__date { display: grid; grid-template-columns: auto minmax(142px, 168px) 40px; align-items: center; gap: 8px; }
.pipeline-report__date label { color: var(--muted); font-size: 12px; font-weight: 650; }
.pipeline-report__date input { min-width: 0; height: 40px; box-sizing: border-box; }
.pipeline-report__date .app-btn--icon { width: 40px; height: 40px; padding: 0; justify-content: center; }
.pipeline-report__meta { margin-top: 6px; color: var(--muted); font-size: 11px; }
.pipeline-metrics { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 10px; margin-top: 18px; }
.pipeline-metric { min-width: 0; min-height: 126px; padding: 13px; border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--surface); }
.pipeline-metric > span { display: block; min-height: 32px; color: var(--muted); font-size: 11px; font-weight: 650; }
.pipeline-metric > strong { display: block; margin: 3px 0 7px; font-size: 28px; line-height: 1; }
.pipeline-metric > div { display: flex; flex-wrap: wrap; gap: 3px 8px; min-height: 20px; }
.pipeline-metric small { display: block; margin-top: 6px; color: var(--muted); font-size: 10px; }
.pipeline-metric--attention { border-left: 3px solid var(--warning); }
.pipeline-metric--positive { border-left: 3px solid var(--success); }
.pipeline-money { display: block; font-size: 12px; font-weight: 650; overflow-wrap: anywhere; }
.pipeline-muted, .pipeline-empty { color: var(--muted); }
.pipeline-empty { margin: 10px 0 0; font-size: 13px; }
.pipeline-section { min-width: 0; margin-top: 28px; padding-top: 20px; border-top: 1px solid var(--line); }
.pipeline-section--split { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 28px; }
.pipeline-section__column { min-width: 0; }
.pipeline-section__heading { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 10px; }
.pipeline-section__heading h2 { margin: 0; font-size: 17px; }
.pipeline-section__heading > span { color: var(--muted); font-size: 11px; text-align: right; }
.pipeline-stage-table, .pipeline-data-table { width: 100%; border-collapse: collapse; table-layout: fixed; }
.pipeline-stage-table th, .pipeline-data-table th { padding: 8px 9px; border-bottom: 1px solid var(--line); color: var(--muted); font-size: 10px; font-weight: 700; text-align: left; text-transform: uppercase; }
.pipeline-stage-table td, .pipeline-data-table td { min-width: 0; padding: 10px 9px; border-bottom: 1px solid var(--line); font-size: 12px; vertical-align: top; overflow-wrap: anywhere; }
.pipeline-stage-table th:nth-child(1) { width: 42%; }
.pipeline-stage-table th:nth-child(2) { width: 15%; }
.pipeline-stage-table td > a, .pipeline-data-table td > a { color: var(--accent); font-weight: 650; }
.pipeline-stage-table td small, .pipeline-data-table td small, .pipeline-record-link small { display: block; margin-top: 3px; color: var(--muted); font-size: 10px; font-weight: 400; }
.pipeline-stage-table td .pipeline-money { font-size: 11px; }
.pipeline-table-wrap { min-width: 0; }
.pipeline-aging { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1px; overflow: hidden; border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--line); }
.pipeline-aging__bucket { min-width: 0; padding: 12px; background: var(--surface); }
.pipeline-aging__bucket > span, .pipeline-aging__bucket > small { display: block; color: var(--muted); font-size: 11px; }
.pipeline-aging__bucket > strong { display: block; margin: 4px 0; font-size: 22px; }
.pipeline-aging__bucket--stale { box-shadow: inset 3px 0 0 var(--warning); }
.pipeline-handoff-counts { display: flex; flex-wrap: wrap; gap: 6px 16px; margin-bottom: 7px; color: var(--muted); font-size: 11px; }
.pipeline-handoff-counts strong { color: var(--text); font-size: 13px; }
.pipeline-state { display: inline-flex; align-items: center; min-height: 24px; padding: 2px 7px; border: 1px solid var(--line); border-radius: var(--radius-sm); font-size: 10px; font-weight: 700; }
.pipeline-state--active { border-color: var(--accent); color: var(--accent); }
.pipeline-state--ready_completed { border-color: var(--success); color: var(--success); }
.pipeline-state--no_project, .pipeline-state--unknown { border-color: var(--warning); color: var(--warning); }
.pipeline-record-link { display: block; color: var(--accent); font-weight: 650; }
.pipeline-record-link + .pipeline-record-link { margin-top: 6px; }
.pipeline-data-table th:nth-child(1) { width: 26%; }
.pipeline-data-table--compact th:nth-child(1) { width: 60%; }
.pipeline-section--evidence details > summary { min-height: 42px; display: flex; align-items: center; color: var(--accent); font-size: 13px; font-weight: 700; cursor: pointer; }
.pipeline-exclusions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1px; border: 1px solid var(--line); background: var(--line); }
.pipeline-exclusions a { min-width: 0; padding: 9px 10px; background: var(--surface); color: var(--text); text-decoration: none; }
.pipeline-exclusions a span, .pipeline-exclusions a strong { display: block; overflow-wrap: anywhere; }
.pipeline-exclusions a span { color: var(--muted); font-size: 10px; }
.pipeline-exclusions a strong { margin-top: 2px; font-size: 12px; }
.pipeline-method { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px 22px; margin-top: 16px; }
.pipeline-method > div { display: grid; grid-template-columns: 94px minmax(0, 1fr); gap: 8px; font-size: 11px; }
.pipeline-method span { color: var(--muted); }

@media (max-width: 1000px) {
    .pipeline-metrics { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

@media (max-width: 720px) {
    .pipeline-report__header { align-items: stretch; flex-direction: column; }
    .pipeline-report__date { grid-template-columns: minmax(0, 1fr) 40px; }
    .pipeline-report__date label { grid-column: 1 / -1; }
    .pipeline-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .pipeline-section--split { grid-template-columns: minmax(0, 1fr); gap: 24px; }
    .pipeline-section__heading { align-items: flex-start; flex-direction: column; gap: 3px; }
    .pipeline-section__heading > span { text-align: left; }
    .pipeline-aging { grid-template-columns: minmax(0, 1fr); }
    .pipeline-exclusions, .pipeline-method { grid-template-columns: minmax(0, 1fr); }
    .pipeline-data-table thead { display: none; }
    .pipeline-data-table, .pipeline-data-table tbody, .pipeline-data-table tr, .pipeline-data-table td { display: block; width: 100%; box-sizing: border-box; }
    .pipeline-data-table tr { padding: 8px 0; border-bottom: 1px solid var(--line); }
    .pipeline-data-table td { display: grid; grid-template-columns: minmax(92px, 34%) minmax(0, 1fr); gap: 8px; padding: 5px 0; border: 0; }
    .pipeline-data-table td::before { content: attr(data-label); color: var(--muted); font-size: 10px; font-weight: 700; text-transform: uppercase; }
}

@media (max-width: 430px) {
    .pipeline-report__header h1 { font-size: 21px; }
    .pipeline-metrics { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 7px; }
    .pipeline-metric { min-height: 118px; padding: 10px; }
    .pipeline-metric > strong { font-size: 24px; }
    .pipeline-stage-table th, .pipeline-stage-table td { padding-left: 5px; padding-right: 5px; }
    .pipeline-stage-table th:nth-child(1) { width: 39%; }
    .pipeline-method > div { grid-template-columns: minmax(0, 1fr); gap: 2px; }
}

/* ============================================================
   Claude 2026-09-03 — generic renderer, mobile touch pass
   ------------------------------------------------------------
   Every control below is emitted by the DATA-DRIVEN surfaces, not by a
   bespoke page: veksi_field_input() (lib/records.php) renders each
   schema_def field, and app/pages/records.php renders the shared list
   chrome for every record_type. Fixing them here lifts every type and
   every future vertical at once.

   Measured defects at 375px before this block:
     .app-input / .app-select / .app-textarea   14px  -> iOS zooms on focus
     .records-toolbar__search .app-input        13px, 4px padding (~27px tall)
     .records-status-filters--wo .records-status-select  14px
     .app-btn--sm (view switcher, status chips, pager)  12px (~25px tall)
     .lookup-item (typeahead result row)        13px, 8px padding (~32px)
     .app-multiselect option label              13px, bare 13px checkbox
   iOS Safari zooms the page whenever a focused text control computes below
   16px (the viewport meta in app/layout.php sets no maximum-scale, so the
   behaviour is live), and WCAG 2.5.8 / the Apple and Material guidance put
   the minimum touch target at 44px.

   Desktop is deliberately untouched: every rule is inside the 640px
   breakpoint, except .app-multiselect__opt, which only restates the inline
   declarations veksi_field_input() used to emit.
   ============================================================ */

/* Base (all widths): identical to the inline style this class replaced.
   Specificity 0,2,0 so it still beats `.app-form-group label` (0,1,1),
   which is what the inline declaration used to do. */
.app-multiselect .app-multiselect__opt {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    margin: 0 14px 6px 0;
    font-weight: normal;
    font-size: 13px;
}

@media (max-width: 640px) {
    /* 1. Schema-rendered fields: no iOS auto-zoom, real touch height.

       THE ZOOM FIX IS FOR ALL THREE; THE 44px FLOOR IS NOT.
       Split 8.9.2026. iOS focus-zoom fires on any focused text control under
       16px, so the font-size belongs to every one of these classes. The
       `min-height: 44px` does not: `.app-textarea` already declares
       `min-height: 80px` at line 1099 — SAME specificity (0,1,0), earlier in
       the file — so grouping it here did not raise a textarea to 44px, it
       LOWERED it from 80px to 44px on exactly the screen where a multi-line
       box has the least room to begin with. An 80px box already clears the
       44px WCAG 2.5.8 target twice over, so nothing is lost by leaving it
       alone, and a phone user typing a rationale or a set of lines keeps the
       two visible rows the desktop gives them.

       This is the failure mode the accounting block at line ~3423 spells out
       in full and guards against with `textarea:not(.app-textarea)`:
       "`min-height` only ever raises a box, never shrinks one — but replacing
       an 80px floor with a 44px floor is a shrink, and it would have been
       invisible in a 'count of controls under 44px'." That block wrote the
       exclusion into its selector; this one had swept the class back in.
       Pinned by tests/accounting_field_targets_test.php §5 and
       tests/payroll_field_targets_test.php, both of which RESOLVE the cascade
       for a `.app-textarea` on a phone rather than grepping for a number. */
    .app-input,
    .app-select,
    .app-textarea {
        font-size: 16px;
    }
    .app-input,
    .app-select {
        min-height: 44px;
    }

    /* The list toolbar's own search box overrides the base .app-input with a
       compact 13px/4px rule (specificity 0,2,0), so it needs the same
       selector to be restored to a usable size. */
    .records-toolbar__search .app-input {
        font-size: 16px;
        padding: 10px 12px;
        min-height: 44px;
    }

    /* Same story for the mobile status <select> that replaces the chip row. */
    .records-status-filters--wo .records-status-select {
        font-size: 16px;
        padding: 10px 12px;
        min-height: 44px;
    }

    /* 2. Lookup typeahead: the result rows are the picking control for every
       `lookup` field (customer, work order, supplier, ...). */
    .lookup-item,
    .lookup-empty {
        display: flex;
        align-items: center;
        min-height: 44px;
        font-size: 15px;
    }
    .lookup-results {
        max-height: 60vh;
    }

    /* 3. Multiselect options: the whole label is the target, so it is the
       label that has to be 44px tall, not just the box. */
    .app-multiselect .app-multiselect__opt {
        display: flex;
        min-height: 44px;
        gap: 10px;
        margin: 0 0 4px 0;
        font-size: 15px;
    }
    .app-multiselect .app-multiselect__opt input[type="checkbox"] {
        width: 22px;
        height: 22px;
        flex: 0 0 auto;
    }

    /* 4. List chrome emitted for every record type: the view switcher, the
       status chips and the pager arrows all use .app-btn--sm (12px, ~25px
       tall). Scoped to these three containers so no other --sm button in the
       app changes. */
    .records-toolbar__views .app-btn,
    .records-status-chips .app-btn,
    .records-pager .app-btn {
        min-height: 44px;
        padding: 10px 14px;
        font-size: 14px;
    }
}

/* ============================================================
   Records list — the mobile sort control, and empty-cell density.

   REGRESSION THIS CLOSES: the record list's sort links live on the
   <th> elements, and the card layout above hides <thead> entirely.
   Measured in Chrome at 375px on the previous commit: 6 sort links
   in the thead, 0 of them reachable, and no sort control anywhere
   else on the page -- reordering a list was impossible on a phone.

   .records-toolbar__sort is display:none by default and only appears
   at the phone breakpoint, so the desktop toolbar is untouched: the
   <th> links stay the only sort affordance above 640px.

   Its options are generated in app/pages/records.php from the same
   _sort_columns() / _sort_link() pair the <th> row uses, so this is
   not a second list of sortable columns.
   ============================================================ */
.records-toolbar__sort { display: none; }

@media (max-width: 640px) {
    .records-toolbar__sort {
        display: flex;
        align-items: center;
        gap: 8px;
        width: 100%;
    }
    .records-toolbar__sort-label {
        flex: 0 0 auto;
        font-size: 13px;
        color: var(--muted);
    }
    /* min-width:0 so a long option label cannot push the flex item
       past the viewport and reintroduce a horizontal scroll. */
    .records-sort-select {
        flex: 1 1 auto;
        min-width: 0;
        width: 100%;
        min-height: 44px;
        font-size: 16px;
        padding: 10px 12px;
    }

    /* Density. A field with no value still cost a full captioned 47px
       row on every card -- measured at 375px, list density had dropped
       from ~7 records per screen to ~2.3, and sparse tenants pay that
       for nothing. `:empty` matches only a cell with NO child nodes and
       NO text at all, so this can never hide a value; the PHP emits the
       cell with no surrounding whitespace precisely so that an
       empty field really is an empty element. Desktop keeps the cell --
       omitting a <td> there would misalign the table's columns. */
    .records-table.admin-mobile-table td:empty { display: none; }
}

/* ============================================================
   Claude 2026-09-03 — APP SHELL, mobile touch pass
   ------------------------------------------------------------
   The five slices before this one fixed the generic record surfaces. They
   did not touch the chrome that wraps EVERY screen, which is what a phone
   user taps most. Measured in Chrome at 375x812 on `88734be`:

     .app-sidebar-nav li a          263x38   (x18 in this workspace)
     .app-sidebar-section (summary) 263x32   (x7, the accordion controls)
     .app-btn ("Uusi Työtilaus")    143x34
     .app-menu-toggle                40x40
     .app-topbar-right a ("Kirjaudu ulos")  80x20
     .app-sidebar-view-as summary    86x17
     .app-sidebar-view-as button    ~50x16   (x6)

   WCAG 2.5.8 / Apple HIG / Material put the minimum target at 44px.

   BREAKPOINT: max-width 900px, not 640px. 900px is where this shell already
   switches to the drawer (`.app-menu-toggle` only exists below it), and the
   existing rule there already states the reason -- "the drawer is driven by
   a fingertip, not a pointer". A tablet holding the same drawer has the same
   fingertip. Desktop (>=901px) is untouched: every sizing rule below is
   inside the media query.

   NOTHING IS HIDDEN to make room. The sidebar list grows from ~700px to
   ~1000px and scrolls inside `.app-sidebar-nav`, which is already
   `overflow-y:auto; min-height:0`.
   ============================================================ */

/* ---- Presentation moved out of layout.php's `style=` attributes ----
   These are the exact declarations that were inline on the elements in
   app/layout.php, so desktop rendering is unchanged. They had to move:
   an inline declaration outranks every stylesheet rule including a media
   query, so no breakpoint could ever have resized these controls. */
.app-sidebar-switch {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    margin: 0 16px 8px;
    padding: 2px 0;
    font-size: 11px;
    color: var(--muted);
    text-decoration: none;
}
/* The inline `text-decoration:none` also beat `a:hover`; keep that. */
.app-sidebar-switch:hover { text-decoration: none; }
.app-sidebar-switch__icon { font-size: 14px; }

.app-sidebar-view-as {
    margin: 0 16px 12px;
    font-size: 11px;
    color: var(--muted);
}
.app-sidebar-view-as__toggle {
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    gap: 4px;
    list-style: none;
}
.app-sidebar-view-as__icon { font-size: 14px; }
.app-sidebar-view-as__options {
    margin-top: 6px;
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding-left: 18px;
}
.app-sidebar-view-as__form { margin: 0; }
.app-sidebar-view-as__btn {
    background: none;
    border: none;
    color: var(--muted);
    cursor: pointer;
    padding: 2px 0;
    font-size: 11px;
    text-align: left;
}

@media (max-width: 900px) {
    /* 1. Navigation. 18 links at 38px was the most-tapped control in the
       product and the furthest under the rule. */
    .app-sidebar-nav li a {
        min-height: 44px;
        padding: 9px 12px;
        font-size: 15px;
    }
    .app-sidebar-nav .nav-icon { font-size: 22px; width: 24px; }

    /* The section headings are `<summary>` elements -- tapping one opens or
       closes a whole product area, so they are controls, not labels. They
       were 32px. (Not in the director's table; found while measuring.) */
    .app-sidebar-section {
        min-height: 44px;
        padding: 8px 12px;
        font-size: 12px;
    }

    /* 2. Every button in the app, which is what the first mobile slice
       deliberately left undone. `--sm` is restated so a compact button stays
       visibly compact while still getting a 44px hit area; on a phone it
       grows 12px -> 13px and 4px 10px -> 8px 12px.

       NOT restated: `.app-btn--worker-action`. Its declared 15px / 10px 20px
       is ALREADY dead everywhere -- it is defined at ~line 908, BEFORE
       `.app-btn` at ~line 980, with the same specificity (0,1,0), so
       `.app-btn` wins and the button actually computes 13px / 8px 16px.
       Verified in Chrome at 1280px on `88734be`; only its min-width:140px
       survives. Restating it here would have made the phone 15px while the
       desktop stayed 13px -- a behaviour change wearing a touch fix as a
       costume. It gets the same treatment as every other button and grows to
       14px, which is still larger than the 13px it renders today. The dead
       modifier is reported as a separate finding, because fixing it properly
       changes desktop and this slice may not. */
    .app-btn {
        min-height: 44px;
        padding: 10px 16px;
        font-size: 14px;
    }
    .app-btn.app-btn--sm {
        padding: 8px 12px;
        font-size: 13px;
    }
    /* The demo banner pinned its buttons to 40px, which beats `.app-btn` on
       specificity. Same selector, so the 44px wins here. */
    .app-demo-banner .app-btn { min-height: 44px; }

    /* 3. Topbar. The toggle is the only way to reach navigation on a phone. */
    .app-menu-toggle {
        width: 44px;
        height: 44px;
        /* `width: 44px` alone did NOT hold, and the shortfall was invisible on
           every screen with a short title. `.app-topbar-left` is a flex row
           (gap 12px) holding this button and the page <h1>. Both are flex items
           at the default `flex-shrink: 1`, so when the title is long enough to
           overflow the row, the browser takes the missing pixels out of BOTH —
           including the button. Measured at 375x812 on the work-order detail
           /app/record/537935, title "Lämmityskattilan huolto, Koivutie 4":
           the toggle rendered 38.88x44, while the same button on the list page
           ("Työtilaukset") measured a correct 44x44. A navigation control must
           not get narrower because the record it sits above has a long name.
           `flex-shrink: 0` pins it; the <h1> keeps absorbing the overflow. */
        flex-shrink: 0;
    }

    /* "Kirjaudu ulos" is the ONLY sign-out control on a phone (the sidebar
       footer shows the name and role, no action), so it has to be a real
       target. It stays a link rather than becoming a button: the topbar is
       56px tall, so 44px fits with 6px clearance, and giving it button
       chrome would make it compete with the page's primary action. Only the
       hit area grows; the appearance does not change. */
    .app-topbar-right a {
        display: inline-flex;
        align-items: center;
        min-height: 44px;
        padding: 0 6px;
        margin-right: -6px;
        font-size: 14px;
    }

    /* 4. "Katso roolina". Sized as real product, not as an operator tool:
       it is documented for owners in the customer user guide
       (docs/kayttoohje/01-perusteet.md 1.8, and 16.x cites it as a step),
       and it is gated on the owner role rather than on any debug flag. It
       costs no list density because the `<details>` is closed by default. */
    .app-sidebar-view-as__toggle {
        min-height: 44px;
        font-size: 14px;
    }
    .app-sidebar-view-as__icon { font-size: 20px; }
    .app-sidebar-view-as__btn {
        display: flex;
        align-items: center;
        min-height: 44px;
        padding: 0 4px;
        font-size: 14px;
    }
    .app-sidebar-view-as { font-size: 14px; }

    /* 5. "Vaihda työtila" -- only rendered for a person who belongs to more
       than one workspace, and their only route between workspaces. */
    .app-sidebar-switch {
        display: flex;
        min-height: 44px;
        padding: 0 4px;
        font-size: 14px;
    }
    .app-sidebar-switch__icon { font-size: 20px; }
}

/* ============================================================
   Claude 2026-09-03 — RECORD / WORK-ORDER DETAIL, mobile pass
   ------------------------------------------------------------
   The screen a field-service worker actually lives in. Measured in Chrome
   at 375x812 on `181e071` (the app-shell slice), on the seeded work order
   /app/record/538001, in all three states it can be in:

     default view   18 controls, 2 under 44px, 3 of 3 inputs under 16px
     edit open      25 controls, 4 under 44px, 1 of 10 inputs under 16px
     "Kirjaa tunnit" open  23 controls, 2 under 44px, 3 of 6 inputs under 16px

   The union of the defects, and what each one is:

     .app-breadcrumb a          x2   72x18 / 51x18, 12px   navigation
     assignee <select>               140x44 but font 12px  iOS focus-zoom
     pool_team_id <select>           105x44 but font 12px  iOS focus-zoom
     workflow assignee <select>      309x44 but font 13px  iOS focus-zoom
     .js-speech-mic             x2   34x28                 dictation button

   FOCUS-ZOOM IS A DEFECT, NOT A PREFERENCE, and it is treated first: iOS
   Safari zooms the page whenever a focused control computes under 16px, and
   app/layout.php's viewport meta sets no maximum-scale, so the page jumps
   under the user's thumb every time they touch one of those three selects.

   ALL THREE were already covered by the generic 640px `.app-input` rule
   (~line 2928) and it could never reach them: every one carried an INLINE
   `style="font-size:12px"` / `13px`, and an inline declaration outranks
   every stylesheet rule including a media query. That is the third time in
   one day this exact mechanism has blocked a mobile fix (veksi_field_input(),
   then app/layout.php, now here). The inline declarations move into the
   classes below, unchanged, so desktop renders byte-identically; the media
   blocks then do what they could not do before.

   NOTHING IS HIDDEN to make room, and nothing is made smaller anywhere.

   TWO BREAKPOINTS, each chosen to match the rules it has to live beside:
     640px — the form controls, because that is where `.app-input`,
             `.app-select` and `.app-textarea` already change size, so the
             whole edit form still changes at exactly one width.
     900px — breadcrumb and dictation button, because that is where this
             page is already finger-driven: `.app-detail-grid` collapses to
             one column there and the shell is a drawer there.
   Desktop (>=901px) is untouched; every sizing rule is inside a media query.
   ============================================================ */

/* ---- Presentation moved out of `style=` attributes, values unchanged ----
   `.app-detail-select`  was  style="padding:2px 8px; font-size:12px; width:auto;"
   `.app-workflow-input` was  style="font-size:13px; width:100%;"
                              (width:100% is already `.app-input`'s own value,
                               so it is not restated here)
   Both sit AFTER the 640px `.app-input` block, so at equal specificity they
   win there too -- which is why each one is restated in the media block
   below. Left out, this file would silently reinstate the 12px/13px it was
   written to remove. */
.app-detail-select {
    padding: 2px 8px;
    font-size: 12px;
    width: auto;
}

.app-workflow-input {
    font-size: 13px;
}

@media (max-width: 640px) {
    /* 1. The three selects that fire iOS focus-zoom. 16px is the threshold,
       not a rounding target: at 15.9px the page still zooms. */
    .app-detail-select {
        font-size: 16px;
        min-height: 44px;
        /* width stays `auto` -- these sit in a wrap-enabled flex row next to
           their submit button, and a full-width select would push that button
           onto its own line. `max-width` is the guard that a long member name
           cannot widen the row past the viewport. */
        max-width: 100%;
    }

    .app-workflow-input {
        font-size: 16px;
        min-height: 44px;
    }
}

@media (max-width: 900px) {
    /* 2. Breadcrumb. Two links, 12px text in an 18px box -- the only way back
       to the record list from a detail page on a phone. They stay text links
       (button chrome next to the page title would compete with the page's own
       actions); ONLY THE HIT AREA CHANGES, the appearance does not -- the
       same treatment "Kirjaudu ulos" got in the shell slice.

       NO horizontal padding, and that is measured rather than tidy: both
       links are already wider than 44px (72px and 51px), so only the height
       was ever short. A first version added `padding:0 6px` plus a
       compensating negative margin; the 24px it added to the row pushed the
       trailing record title onto a second line (row 343x18 -> 343x68). The
       links carry min-height alone, and the row is one 44px line.

       The font stays at 12px on purpose. A 14px version was written first
       and measured: the trailing <span> carries the full record title, so
       14px wrapped the row to two lines and cost 53px of page height at
       375px instead of 26px. Legibility was my addition, not the defect;
       the defect was the 18px target, and that is what is fixed here. */
    .app-breadcrumb a {
        display: inline-flex;
        align-items: center;
        min-height: 44px;
    }

    /* 3. Dictation. `.js-speech-mic` is built in assets/speech-input.js and
       sized by an inline cssText there, so it is 34x28 on every screen. It is
       the most mobile control in the product -- a worker on a roof talking
       into a work order instead of typing.

       The VISIBLE button is deliberately not enlarged. It is absolutely
       positioned over the top-right corner of the textarea, so a 44x44 chip
       would sit on top of two lines of the user's own text: solving a sizing
       problem by covering content is the same mistake as solving it by
       hiding a column. Instead the ACTIVATION area is extended to 44x44 with
       a transparent pseudo-element. The offsets are measured against the
       button's PADDING box, not its border box, so the 1px border on each
       side is part of the sum: 32+2*6=44 wide, 26+2*9=44 tall. WCAG
       2.5.8 measures the region that accepts a pointer action, not the ink.
       This is verifiable and is verified: document.elementFromPoint() at the
       eight compass points of the 44x44 region returns the button. On
       `181e071` the same probe returns none of them.
       (The probe must scrollIntoView first: elementFromPoint takes VIEWPORT
       coordinates and returns null for anything below the fold -- my first
       run reported a clean 'nothing hits' for both trees because of it.)

       The button is already `position:absolute`, so it is its own containing
       block and this needs no change to speech-input.js. */
    .js-speech-mic::after {
        content: "";
        position: absolute;
        top: -9px;
        right: -6px;
        bottom: -9px;
        left: -6px;
    }
}

/* ============================================================
   LINE-ITEM EDITORS — the inline `font-size` sweep (2026-09-03)
   ------------------------------------------------------------
   FOURTH occurrence of the same root cause in one day. An inline `style=`
   declaration outranks every stylesheet rule, media queries included, so no
   breakpoint can ever reach it. It blocked veksi_field_input(), then the
   multiselect, then app/layout.php's shell controls, then the work-order
   detail's three selects. It also blocks EVERY control in the invoice,
   proposal and expense line editors: each one is emitted with
   `style="font-size:12px"` (or 13px), which is exactly the condition that
   makes iOS Safari zoom the page on focus -- app/layout.php's viewport meta
   sets no maximum-scale, so the page jumps under the user's thumb on every
   single field of a line they are typing into.

   The declarations move here UNCHANGED, so desktop renders byte-identically;
   the 640px block then does what the inline attribute would not let the
   existing `.app-input` rule (~line 2928) do.

   Only the TYPOGRAPHY moves. `width`, `max-width`, `text-align` and `flex`
   stay in the markup: they differ per control (72px / 90px / 100px / 64px /
   100% / 240px), they are layout rather than sizing, and at <=640px
   `.line-items-edit-table td input` already forces them all to
   `width:100% !important` anyway.

   NOTHING IS HIDDEN and nothing is made smaller. The mobile block below
   restates exactly what `.app-input` already says at <=640px -- 16px and a
   44px minimum height -- and adds nothing of its own. These classes sit
   AFTER that block, so at equal specificity they would otherwise win there
   and silently reinstate the 12px this sweep exists to remove.
   ============================================================ */

/* Was: style="font-size:12px; padding:4px 6px;" on every line-editor control
   in invoice-detail.php, proposal-detail.php, expense-detail.php and the
   catalog typeahead built in assets/catalog-pick.js. */
.app-input--line {
    font-size: 12px;
    padding: 4px 6px;
}

/* Was: style="font-size:13px;" on the four receipt-scan preview fields in
   expense-detail.php (rp-date / rp-description / rp-amount / rp-vat). */
.app-input--preview {
    font-size: 13px;
}

/* Was: style="font-size:12px;" on the read-only public-acceptance URL field
   in proposal-detail.php. No padding: that control keeps `.app-input`'s own
   8px 12px, and restating it here would change desktop. */
.app-input--sm {
    font-size: 12px;
}

/* Was: style="font-size:12px; padding:3px 6px;" on the twenty-one controls
   shift-template-detail.php's renderDayGrid() builds in JavaScript — three
   per weekday, seven weekdays. One pixel less vertical padding than
   `.app-input--line`, which is why it is restated rather than merged into
   it: merging would change what the schedule grid renders on desktop, and
   this sweep is only allowed to change the phone. */
.app-input--grid {
    font-size: 12px;
    padding: 3px 6px;
}

/* Was: style="width:auto; font-size:13px;" (channel select, next-action
   date) and style="font-size:13px; margin-bottom:8px;" (summary textarea) on
   the three controls of lead-detail.php's "log activity" form -- the last
   surviving instance of the inline-font-size mechanism this block was opened
   to close. The declaration is identical to `.app-input--preview` above and
   is deliberately NOT merged with it: every class in this block is named for
   the markup it was lifted out of, which is what makes the block auditable.
   `width:auto` and `margin-bottom` stay in the markup -- layout, not sizing,
   exactly as this block's header says. */
.app-input--activity {
    font-size: 13px;
}

@media (max-width: 640px) {
    .app-input--line,
    .app-input--preview,
    .app-input--sm,
    .app-input--grid,
    .app-input--activity {
        font-size: 16px;
        min-height: 44px;
    }
}

/* Was: style="background:none; border:none; cursor:pointer;
   color:var(--muted); font-size:16px;" on the remove-line ("✕") button that
   invoice-detail.php, proposal-detail.php and osto-detail.php each build in
   JavaScript. It carried NO class at all, so no stylesheet rule — media
   query or otherwise — could reach it, and it measured 25.1x18.0 on the two
   sale-side pages and 25.1x23.0 on the purchase side. Deleting a line was
   the smallest target on any of the three pages.

   The declarations move here unchanged, so all three render exactly as they
   did on desktop. invoice-detail.php and proposal-detail.php additionally
   carry `line-height:1`, which osto-detail.php does not; that one difference
   stays in the markup rather than being unified here, because unifying it
   would shrink the purchase page's button on desktop and this sweep changes
   only the phone. It blocks nothing: the phone rule below sets the box with
   inline-flex + min-height, which does not depend on line-height. */
.app-line-remove {
    background: none;
    border: none;
    cursor: pointer;
    color: var(--muted);
    font-size: 16px;
}

@media (max-width: 640px) {
    .app-line-remove {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        min-width: 44px;
        min-height: 44px;
    }
}

/* ------------------------------------------------------------
   THE CATALOGUE TYPEAHEAD (assets/catalog-pick.js) — one widget,
   FOUR surfaces, one defect seen on each of them.
   ------------------------------------------------------------
   The section above already named this widget as carrying the same inline
   `font-size` defect, but only its INPUT was reachable: the input has classes
   (`app-input app-input--line`), so `.app-input--line`'s 640px rule already
   sized it. Everything the widget BUILDS did not. `initOne()` wrote

       row.style.cssText = 'padding:7px 10px; cursor:pointer; font-size:12px;
                            border-bottom:1px solid var(--line,#eee);'

   on every result row, and an equivalent `style="…font-size:12px"` on the
   "Ei osumia" row. Inline declarations outrank every stylesheet rule and every
   media query, so no breakpoint could reach them and nothing here could have
   fixed it. The rows are the tap targets that FINISH the catalogue path: on a
   phone they measured 12px text in a ~30px box.

   WHERE THIS WIDGET RENDERS — all four are fixed by this one block, because
   the styles now come from here instead of from four copies of the same
   inline string:
     app/pages/work-order-detail.php  (the worker's material form)
     app/pages/invoice-detail.php     (the sale line editor)
     app/pages/proposal-detail.php    (the proposal line editor)
     app/pages/osto-detail.php        (the purchase line editor)
   tests/work_order_parts_journey_test.php measures the first two; the last two
   mount the identical widget from the identical `[data-catalog-pick]` host.

   DESKTOP IS BYTE-IDENTICAL. Every declaration outside the 640px block below
   is the value catalog-pick.js used to write inline, unchanged and in the same
   order. Nothing here is new above 640px.

   NO `display` CHANGE ON THE PHONE, deliberately. A row is a block <div>, so
   `min-height` applies to it; switching it to flex to centre the text would
   turn `<strong>name</strong><span> (code)</span>` into two flex items and
   collapse the leading space inside the span, which would read as
   "Silikonimassa(SIL-310)". The extra padding centres a single line instead
   and a wrapped one simply grows past the 44px floor.

   ONE CONSEQUENCE, MEASURED AND ACCEPTED RATHER THAN DISCOVERED LATER. The
   host's `display` used to be written inline, so it beat every stylesheet.
   Declared here at `[data-catalog-pick]` (0,1,0) it is outranked on ONE of the
   four surfaces by line 3327, `.worker-material-form label > span` (0,2,1) —
   the host is a <span> and a direct child of that label. So on the work-order
   material form the host now computes `block` instead of `inline-block`.
   Measured at 375x812: the dropdown widens 260 -> 311px and ends at x=344 in a
   375px viewport, so nothing escapes; at 320 and at 1280 every number is
   unchanged, on both surfaces. On the invoice, proposal and osto surfaces the
   host sits in a `display:flex` row, where `inline-block` is blockified to
   `block` anyway — measured on the invoice, and the other two carry the same
   markup.
   It is left alone ON PURPOSE. Pinning it would mean either a page-scoped
   `.worker-material-form__catalog [data-catalog-pick]` override — a fix scoped
   to one page's container, which is the exact mistake cb24135 exists to undo —
   or repeating the attribute selector to outrank a rule that is not wrong. A
   wider dropdown on a phone is not a regression, and desktop did not move. */

[data-catalog-pick] {
    position: relative;
    display: inline-block;
}

.catalog-pick-input {
    min-width: 200px;
}

.catalog-pick-box {
    position: absolute;
    z-index: 50;
    left: 0;
    top: 100%;
    min-width: 260px;
    background: var(--card, #fff);
    border: 1px solid var(--line, #ccc);
    border-radius: 6px;
    box-shadow: 0 4px 16px rgba(0, 0, 0, .12);
    display: none;
    max-height: 280px;
    overflow: auto;
}

.catalog-pick-row {
    padding: 7px 10px;
    cursor: pointer;
    font-size: 12px;
    border-bottom: 1px solid var(--line, #eee);
}

.catalog-pick-row__meta {
    color: var(--muted, #888);
}

.catalog-pick-empty {
    padding: 8px 10px;
    color: var(--muted, #888);
    font-size: 12px;
}

@media (max-width: 640px) {
    .catalog-pick-row,
    .catalog-pick-empty {
        font-size: 16px;
        min-height: 44px;
        /* 12 + a ~19px line + 12 = 43px of content in a 44px border-box, so a
           single-line row reads as centred without a `display` change. */
        padding: 12px 10px;
    }
}

/* ============================================================
   WORK-ORDER DETAIL PAGE — mobile, measured in a real browser.

   Every number below came off the RENDERED page: headless Chrome at
   375x812 and 320x812, `getComputedStyle()` and `getBoundingClientRect()`
   on the DOM the server actually served. Not a source grep, and not the
   PHP cascade model the four earlier mobile suites use — that model
   cannot see layout, and layout is where three of these defects lived.
   tests/work_order_detail_rendered_mobile_test.php replays the same
   measurement and fails if any of them comes back.

   MEASURED DEFECTS on 558ec08, /app/record/<id> at 375px:

   1. FOCUS-ZOOM — the defect, not a preference. Eight controls in the
      billing-rule form (?edit_billing_rule=1) computed 13.3px: they carry
      an inline width and NO class at all, so `.app-input`'s 16px phone
      rule never applied to them. iOS Safari zooms the viewport whenever a
      focused control is under 16px and app/layout.php's viewport meta
      sets no maximum-scale, so setting a job's hourly rate on a phone
      zoomed the page on every one of the eight fields and left the user
      pinching back out between them. Fixed in the PHP by giving the seven
      text inputs the `.app-input` class they should always have had — no
      new CSS at all — and the checkbox the `.app-check` label below.

   2. `.app-icon-btn` was an UNDEFINED CLASS. Two controls on this page
      use it (the material form's close button and the per-row correction
      <summary>) and nothing in this stylesheet ever defined it, so both
      rendered at the browser's default 40x33. Defining it once is the
      whole fix; there was no rule to override.

   3. Document category filters: nine bare links, 19.5px tall, in a row
      whose vertical gap is 6px — nine 44px targets' worth of intent in
      171px of reachable height. The active-state colour also lived in a
      PHP ternary; the page already emits `aria-current`, so the ternary
      is gone and the stylesheet reads that attribute instead.

   4. Two bare links inheriting 13px inside styled containers:
      `.app-worker-location a` (the job address the field worker taps to
      navigate — 35.5px) and `.app-job-status-value a` (the customer link
      — 37.5px, and 17px in the office layout).

   NOT a defect, checked and left alone: `.js-speech-mic` renders 34x28
   but its `::after` measures a real 44x44 at -9px/-6px insets, which is
   the activation area. It is deliberately small so a 44px chip cannot
   cover the user's own text in the textarea it sits on. The suite
   resolves pseudo-elements so it does not "fix" this one.

   Placed at the END of the file on purpose. Every rule here is inside the
   640px breakpoint except the two base declarations, and the failure this
   guards against is a later declaration of equal specificity quietly
   winning against an earlier media query — from the end of the file there
   is no later declaration. Desktop is untouched: nothing outside the two
   base rules applies above 640px, and both of those replace UA defaults
   on markup that had no styling of its own.
   ============================================================ */

/* A checkbox and its label are ONE control: the label is what a thumb
   actually lands on, so it is the label that has to be 44px, not the
   13px box. Mirrors `.app-multiselect__opt`, which solved the same
   problem for schema-rendered multiselects. */
.app-check {
    display: flex;
    gap: 6px;
    align-items: center;
}

/* Used by app/pages/work-order-detail.php for the material form's close
   button and the correction <summary>. Previously defined nowhere. */
.app-icon-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    padding: 0;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--paper);
    color: var(--ink);
    cursor: pointer;
    list-style: none;
}
.app-icon-btn::-webkit-details-marker { display: none; }

.wo-doc-filter { color: var(--muted); }
.wo-doc-filter[aria-current="page"] { font-weight: 600; color: #1f2937; }

@media (max-width: 640px) {
    /* 1. Focus-zoom: the checkbox and its label. The 16px is what stops
       the zoom; the 44px is what makes it tappable. */
    .app-check {
        min-height: 44px;
        gap: 10px;
        font-size: 16px;
    }
    /* font-size is restated on the box itself, not just inherited from the
       label: the UA stylesheet sets `font` on form controls, so a checkbox
       keeps 13.3px however large its label text is. Measured: the label
       alone left the box at 13.3px. */
    .app-check input[type="checkbox"] {
        width: 22px;
        height: 22px;
        flex: 0 0 auto;
        font-size: 16px;
    }

    /* 2. Icon buttons and the correction summary. */
    .app-icon-btn {
        width: 44px;
        height: 44px;
    }

    /* 3. Document category filters become real chips. The row's own
       flex/wrap/gap stays inline in the PHP — tests/work_order_document_filter_test.php
       pins that container shape and it is still the correct shape; only the
       links themselves change here. */
    .wo-doc-filters { gap: 8px; }
    .wo-doc-filter {
        display: inline-flex;
        align-items: center;
        min-height: 44px;
        padding: 0 14px;
        font-size: 15px;
        border: 1px solid var(--line);
        border-radius: 999px;
        text-decoration: none;
    }

    /* 4. Bare links inside styled containers. `display:inline-flex` is
       what makes min-height apply at all — on an inline box it does
       nothing, which is why these measured 35.5px and 17px tall.

       `.app-detail-field .value a` is NOT specific to this page: that is
       the GENERIC RENDERER's read-only field, so a linked value on every
       record type — customer, supplier, work order, invoice — was a 17px
       target on a phone. Measured on this page first, fixed for all of
       them. tests/record_detail_mobile_touch_test.php covers the same
       container's inputs; this is its read side. */
    .app-worker-location a,
    .app-job-status-value a,
    .app-detail-field .value a {
        display: inline-flex;
        align-items: center;
        min-height: 44px;
        font-size: 15px;
    }
}

/* ============================================================
   Claude 2026-09-04 — the generic renderer's SHARED COMPONENTS
   ------------------------------------------------------------
   HOW THESE FOUR WERE FOUND. tests/generic_renderer_rendered_mobile_test.php
   renders twenty surfaces in headless Chrome at 375x812 and 320px and reads
   getComputedStyle()/getBoundingClientRect() off the live DOM. The surfaces
   are not a guess: they are the call sites of the engine's two field
   renderers, `veksi_field_input()` and `veksi_field_display()`, taken from

     $ grep -rn 'veksi_field_input\|veksi_field_display' app/ lib/

   plus the four shipping record types (`contact`, `maintenance_plan`,
   `visit_target`, `vacation_request`) that no bespoke page intercepts, so
   /app/record/{id} renders record-detail.php — the engine and nothing else.

   THE SHAPE OF ALL FOUR DEFECTS IS THE SAME, and it is worth naming once: a
   SHARED component was measured on ONE page and fixed with a rule scoped to
   that page's container. The component then kept the defect everywhere else.
   That is the pattern this block closes; the rules below are keyed on the
   component, not on the page that happened to be measured.

   Measured at 63c1c60, before this block (375px, then 320px identical
   unless noted):

     .app-multiselect__opt input[type=checkbox]   13.3px font
         3 surfaces: the engine's own multiselect on /app/record/{id} edit,
         on the generic create form, and on customer-detail. The label around
         it is already 15px and 44px tall — the UA stylesheet sets `font` on
         form controls directly, so the label's size never reaches the box.
         `.app-check input[type="checkbox"]` was given exactly this line on
         2026-09-03 for exactly this reason; the engine's own checkbox group
         was not, and the two disagreed.

     details > summary                            309x18, 254x19.5
         2 surfaces: the LEGACY-FIELD disclosure that record-detail.php emits
         for every record type whose data carries keys the schema no longer
         declares, and customer-detail's e-invoice details. 17 <summary>
         elements exist across app/pages; one of them, work-order-detail's
         material-correction toggle, is an `.app-icon-btn` and is already
         44x44 — it is excluded by selector rather than by hoping.

     .app-table-link                              305x15, 238x33
         records-time-week's work-order column. The identical defect in the
         records list was fixed on 2026-09-03 by a rule scoped to the records
         list's own phone-card table and its FIRST CELL only — see the 640px
         block above, whose comment says "as inline text it was a 19px-tall
         target". The class is shared by seven pages; that rule reached one
         table.
         (Its selector is deliberately not quoted here.
         records_table_mobile_cards_test.php strips media-block BODIES from
         app.css and greps the remainder for that selector, so naming it in a
         comment that sits outside a media block reads to that suite as a
         leaked desktop rule. Measured: the first version of this comment
         turned that suite red while changing no rule at all. That is a real
         weakness in the suite — a comment is not a rule — and it is worth
         fixing there, not by weakening the assertion from here.)

     .app-breadcrumb a                            26.5x44 … 41.8x44
         6 surfaces: purchase 26.5, shift 25.7, lead 28.0, product 32.0,
         invoice 40.6, proposal 41.8. The ≤900px rule above already gives
         these links their 44px HEIGHT, and its comment states the reason it
         adds no width: "both links are already wider than 44px (72px and
         51px), so only the height was ever short." That was true of the two
         labels on the page it was measured on ("Kojelauta", "Työtilaus") and
         is false of the product as a whole — the breadcrumb's middle link is
         the record TYPE's label, so its width is a property of the type, not
         of the component. Short type names are the common case.

   WHAT IS DELIBERATELY NOT CHANGED. The ≤900px block keeps its 12px font and
   its no-horizontal-padding decision; both were measured and both are still
   right (14px wrapped the row; padding pushed the trailing record title onto
   a second line). `min-width` widens the hit area without moving the text's
   own box, so the row grows only for links narrower than 44px.

   Desktop is untouched: every rule is inside a media query, and this block is
   the LAST in the file, so at equal specificity it wins rather than losing to
   a later declaration — the failure mode that made a sibling slice's fix look
   exactly like success.
   ============================================================ */

@media (max-width: 640px) {
    /* 1. The engine's own multiselect box. Width/height already come from the
       2026-09-03 block; only the font was left at the UA's 13.3px. */
    .app-multiselect .app-multiselect__opt input[type="checkbox"] {
        font-size: 16px;
    }

    /* 2. Disclosure triangles. `display` is deliberately left alone: a
       <summary> is `display: list-item`, which is block-level, so min-height
       applies to it as-is — and switching it to flex would delete the
       disclosure marker, solving a touch problem by removing an affordance.
       Symmetric padding does the centring instead. A summary that already
       carries an inline `padding` shorthand keeps it (inline wins) and still
       gets the 44px floor. */
    summary:not(.app-icon-btn) {
        min-height: 44px;
        padding-top: 13px;
        padding-bottom: 13px;
        box-sizing: border-box;
    }

    /* 3. The list-cell record link, on the component rather than on one
       table. Same three declarations the admin-mobile-table rule already
       uses, so the card layout is unchanged and every other table catches up
       to it. */
    .app-table-link {
        display: flex;
        align-items: center;
        min-height: 44px;
    }
}

@media (max-width: 900px) {
    /* 4. Breadcrumb WIDTH — the half the ≤900px rule above could not know it
       needed, because the middle link's text is the record type's label and
       the page it was measured on happened to have a long one. */
    .app-breadcrumb a {
        min-width: 44px;
        justify-content: center;
    }
}


/* ============================================================
   Claude 2026-09-05 — the KNOWN register of
   tests/generic_renderer_rendered_mobile_test.php, worked down.

   Every rule below closes a REGISTERED defect that was re-measured in a real
   browser at 375px and 320px on 2fae6e3 before it was touched, and each is
   written on the COMPONENT rather than on the page that happened to be
   measured, because in three of the four cases the same markup ships on a
   second surface the suite does not render:

     .app-period-nav        records-time-week.php AND report-time-saldo.php
                            -- the same single-glyph pager, 31.2x44. Only the
                            first is a subject of that suite, so a fix scoped
                            to it would have left the saldo report broken and
                            nothing would have said so.
     .app-shift-brief-item  lead-detail.php AND shift-detail.php. Only the
                            lead card puts anchors in it today; the rule is
                            on the component so the shift card cannot acquire
                            the defect by adding one.
     .app-alert a           every alert in the app. Measured on
                            work-order-detail.php's proposal-chain alert
                            (308.8x35.5 at 375, 183.9x35.5 at 320).
     .app-inv-track-step    the invoice lifecycle tracker; work-order-detail
                            is its only host today.

   `.app-inv-summary` is the one page-local rule here, and it is page-local
   because the markup is: work-order-detail.php builds that row once, inline,
   and nothing else emits it.

   WHY min-height AND display. `min-height` does nothing on an inline box, so
   every rule that raises a link to 44px sets `display` first. That is the
   defect the 63c1c60 `.app-detail-field .value a` rule was written for and
   the one this file keeps relearning.

   NOTHING USES !important. None of these elements carries an inline
   font-size, height, min-height or display that a stylesheet would have to
   fight; the inline attributes they do carry are colour, weight, margin and
   the flex layout of their own container, and all of those are left standing.

   Desktop is untouched: every rule is inside the 640px breakpoint, and this
   block is the LAST in the file, so at equal specificity it wins rather than
   being silently overridden by a later declaration.
   ============================================================ */

@media (max-width: 640px) {
    /* 1. Links inside an alert. An alert is how the app tells someone that
       this job came from a proposal, or from a maintenance plan -- and the
       link out of it was a 35.5px-tall inline box. */
    .app-alert a {
        display: inline-flex;
        align-items: center;
        min-height: 44px;
        /* An inline-flex box is max-content wide, so without this cap a link
           label longer than the alert would stop wrapping the moment it stops
           being `display: inline` -- a regression this rule would otherwise
           introduce on every alert whose link text is longer than the one
           measured here (183.9px at 320px). It can only ever prevent overflow. */
        max-width: 100%;
    }

    /* 2. The invoice lifecycle tracker's current step. The anchor wraps an
       18x18 dot and measured 18.0x23.0 -- the smallest target on the page.

       It is widened by a transparent ::after overlay rather than by growing
       the anchor's own box, and that is not a trick to satisfy the measure:
       a pseudo-element is part of its originating element for hit-testing,
       so the 44x44 really is tappable. Growing the box instead would move
       the dot's centre away from `top: 9px`, where .app-inv-track-step's
       ::before connector line is drawn, and knock the tracker's rail out of
       line with its dots.

       OVERLAP, checked rather than assumed: a step is ~68px wide at 375px
       and ~64px at 320px, so a 44px overlay centred in it does not reach the
       neighbouring step -- and only the CURRENT step is ever a link, so
       there is no neighbouring target to reach. Below it sits the step's own
       label, which is text, not a control. */
    .app-inv-track-step > a {
        position: relative;
        display: block;
    }
    .app-inv-track-step > a::after {
        content: '';
        position: absolute;
        left: 50%;
        top: 50%;
        width: 44px;
        height: 44px;
        transform: translate(-50%, -50%);
    }

    /* 3. The contact strip: mail / call / SMS on a lead. These are the
       reason someone opens a lead on a phone at all, and they measured
       279.0x42.0, 272.8x42.0 and 26.3x18.0.

       The anchors need `min-height` and nothing else, because they are
       already flex ITEMS of `.app-shift-brief-item` and therefore already
       blockified — `display: block`, measured, not assumed. `display: flex`
       is here only to centre the text against the taller box. */
    .app-shift-brief-item a {
        display: flex;
        align-items: center;
        min-height: 44px;
        max-width: 100%;
    }

    /* `flex-wrap` is what keeps the SMS link's new 44px width from pushing
       the row past the card: that item is a nowrap flex row and 272.8 + 4 +
       44 does not fit the 311px of card interior a 375px viewport leaves.

       IT IS SCOPED TO THE ONE ROW THAT NEEDS IT, and that scoping was earned
       by measurement rather than caution. Put on `.app-shift-brief-item`
       itself, `flex-wrap` made EVERY row wrap: a flex line breaks on an
       item's hypothetical main size, which is its max-content width, so a
       contact name or e-mail longer than the row stopped being shrunk onto
       one line and moved below its own icon instead. The brief card went
       248px -> 402px tall at 375px with the icons orphaned above their text.
       Only the call/SMS pair actually overflows, so only it wraps. */
    .app-shift-brief-item--actions {
        flex-wrap: wrap;
    }
    .app-shift-brief-sms {
        min-width: 44px;
        justify-content: center;
    }

    /* 4. The period pager. Both hosts lay the row out with an inline
       `display:flex; align-items:center; gap:10px`, which this rule leaves
       alone; it adds the wrap the row never had, so the two arrows can grow
       from 31.2px to 44px without squeezing the week/month label or pushing
       the total past the right edge. */
    .app-period-nav {
        flex-wrap: wrap;
    }
    .app-period-nav .app-btn {
        min-width: 44px;
        min-height: 44px;
        justify-content: center;
    }

    /* 5. The invoice summary row on a work order -- number, customer, total,
       due date, status. The number is the link, and it measured 283.0x39.0
       as a flex item taking the row's own height. */
    .app-inv-summary a {
        display: inline-flex;
        align-items: center;
        min-height: 44px;
        max-width: 100%;
    }
}

/* ============================================================
   Claude 2026-09-13 — PÄÄKIRJAN TAULU KAPEALLA NÄYTÖLLÄ
   ------------------------------------------------------------
   Mitattu selaimessa 298 CSS-pikselin leveydellä. Kaksi asiaa, ei
   yhtä. (1) Pääkirjan viisi saraketta vaativat 271 px, kortin
   sisäleveys on 249 px. (2) Otsikon saldo on float:right, ja
   overflow-x: auto tekee seuraavasta laatikosta oman
   lohkomuotoilukontekstinsa — sellainen KAVENTUU kelluvan viereen,
   mitattuna 99 px:iin, jolloin taulu vieri 99 px:n ikkunassa. Siksi
   clear: both; ilman sitä pelkkä taulun kaventaminen ei olisi
   auttanut, ja mittari olisi näyttänyt vihreää väärästä syystä.
   Sivu itse ei vierry sivusuunnassa (body scrollWidth ===
   clientWidth) eikä ylätunnisteessa ole päällekkäisyyttä: mitattu
   .app-topbar-left päättyy x=81, .app-topbar-right alkaa x=174.
   Vain pääkirjan taulu kapenee, ja vain kapealla näytöllä.
   ============================================================ */
.app-table-scroll {
    overflow-x: auto;
    clear: both;
}

@media (max-width: 520px) {
    .app-table--ledger th,
    .app-table--ledger td {
        padding: 8px 4px;
        font-size: 12px;
    }
    /* Päivä SAA katketa. Ensin pakotin sen yhdelle riville, ja
       mitattuna se söi 76 px viidestä sarakkeesta: taulu jäi 244
       px:iin, kun kortissa on 217. Katkeavana se vie noin 40 px, ja
       kaikki viisi saraketta mahtuvat näkyviin ilman vieritystä. */
    .app-table--ledger td:first-child {
        word-break: break-word;
    }
}

/* ============================================================
   KIRJANPIDON TYÖTILA — the page header, the destination's tab row
   and the overview (Kirjanpito UI slice A, 28.9.2026).
   ============================================================
   One compact context area per page: the destination as a small
   label, the task as the one heading, the company, year and working
   month as one line, then the destination's own views as a row of
   plain links that never folds and never opens over the work. The
   overview's first card holds the four answers — period, next
   action, what is recorded, what is open — and everything else is
   one named disclosure below it. Scoped to strip pages
   (.app-shell--own-nav), so no platform page can pick these up. */
.app-shell--own-nav .acc-header {
    align-items: flex-start;
    gap: 12px;
    margin-bottom: 12px;
}

.app-shell--own-nav .acc-header__titles {
    min-width: 0;
}

.app-shell--own-nav .acc-header__area {
    margin: 0 0 2px;
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--muted);
}

.app-shell--own-nav .acc-header__context {
    margin: 4px 0 0;
    font-size: 13px;
    color: var(--muted);
}

.app-shell--own-nav .acc-tabs {
    display: flex;
    flex-wrap: wrap;
    gap: 4px 16px;
    margin: 0 0 16px;
    padding: 0 0 6px;
    border-bottom: 1px solid var(--line);
    font-size: 13px;
}

.app-shell--own-nav .acc-tabs__tab {
    display: inline-block;
    padding: 4px 0;
    color: var(--ink);
    text-decoration: none;
    border-bottom: 2px solid transparent;
}

.app-shell--own-nav .acc-tabs__tab:hover {
    color: var(--accent);
    text-decoration: none;
}

.app-shell--own-nav .acc-tabs__tab.is-current {
    color: var(--accent);
    font-weight: 600;
    border-bottom-color: var(--accent);
}

.app-shell--own-nav .acc-context-note {
    margin: 0 0 16px;
    font-size: 13px;
}

.app-shell--own-nav .acc-context-note p {
    margin: 0 0 4px;
}

.app-shell--own-nav .acc-context-note__scope {
    display: inline-block;
    margin-left: 8px;
    color: var(--muted);
    font-size: 12px;
}

.app-shell--own-nav .acc-overview {
    margin-bottom: 16px;
}

.app-shell--own-nav .acc-overview__title {
    margin: 0 0 6px;
    font-size: 18px;
}

.app-shell--own-nav .acc-overview__headline {
    margin: 0 0 12px;
    font-size: 14px;
}

.app-shell--own-nav .acc-next {
    margin: 0 0 14px;
    padding: 12px 14px;
    border-left: 4px solid var(--accent);
    background: var(--accent-soft);
    border-radius: var(--radius-sm);
}

.app-shell--own-nav .acc-next p {
    margin: 0 0 6px;
    font-size: 14px;
}

.app-shell--own-nav .acc-next p:last-child {
    margin-bottom: 0;
}

.app-shell--own-nav .acc-next__label {
    font-size: 11px !important;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--signal);
}

.app-shell--own-nav .acc-next__hint,
.app-shell--own-nav .acc-next__who {
    color: var(--muted);
    font-size: 12px !important;
}

.app-shell--own-nav .acc-next__hint {
    margin-left: 8px;
}

/* The next action's whole reason, one click away (slice D). */
.app-shell--own-nav .acc-next__why {
    margin: 6px 0 0;
    font-size: 13px;
}

.app-shell--own-nav .acc-next__why summary {
    cursor: pointer;
    min-height: 44px;
    padding: 12px 0;
    box-sizing: border-box;
    color: var(--muted);
}

.app-shell--own-nav .acc-next__why p {
    margin: 0;
}

/* MONEY DOES NOT BREAK ACROSS LINES (Kirjanpito UI slice D, 29.9.2026). The
   formatter writes amounts with an ordinary space between the thousands, so a
   narrow column broke 12 345,67 into "12" and "345,67" — two numbers where
   there is one. On the Kirjanpito pages a right-aligned table cell, the one
   shape every report prints an amount in, and an explicit amount cell keep the
   amount on one line. A card that holds a plain table pans sideways inside
   itself instead, so the page itself still never scrolls sideways; tables that
   stack into labelled cards on a phone keep their own rule. */
.app-shell--own-nav .app-table td[style*="text-align:right"],
.app-shell--own-nav .app-table th[style*="text-align:right"],
.app-shell--own-nav .app-table td.app-table-amount {
    white-space: nowrap;
}

.app-shell--own-nav .app-card:has(.app-table:not(.app-table--cards)) {
    overflow-x: auto;
}

.app-shell--own-nav .acc-sub {
    margin: 12px 0 4px;
    font-size: 14px;
}

.app-shell--own-nav .acc-activity__line {
    margin: 0 0 2px;
    font-size: 13px;
}

.app-shell--own-nav .acc-worklist__list {
    list-style: none;
    margin: 0;
    padding: 0;
    font-size: 13px;
}

.app-shell--own-nav .acc-worklist__list li {
    padding: 6px 0;
    border-top: 1px solid var(--line);
}

.app-shell--own-nav .acc-worklist__why {
    display: block;
    color: var(--muted);
}

.app-shell--own-nav .acc-mode {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    align-items: baseline;
    margin: 0 0 16px;
}

.app-shell--own-nav .acc-more__summary {
    cursor: pointer;
    font-weight: 600;
    font-size: 14px;
}

.app-shell--own-nav .acc-more__body {
    margin-top: 12px;
}

/* ── Ostolaskun tarkistus (29.9.2026, purchase-invoice review) ──────────────
   The selected invoice's document beside its fields on a wide screen; on a
   narrower one (below 1100px, where the 240px workspace rail leaves the two
   panes too little room) a Lasku | Tiedot switch (assets/purchase-review.js) shows
   one pane at a time, and without the script both panes stack with jump links.
   Every table sits in its own horizontal scroller, so the page never scrolls
   sideways. Nothing here is sticky: the shell's .app-main is an overflow
   container that does not scroll itself, so a sticky pane could never engage.
   Instead, on a wide screen the field pane beside a shown document scrolls on
   its own, so the invoice stays in view while the lower fields are filled; on a
   narrow one "Näytä lasku" by the lower fields reaches the invoice from there,
   and the script brings the switch back under the top bar. */
.pur-review {
    padding: 16px;
    margin-bottom: 16px;
}

.pur-review__head h3 {
    margin: 0 0 6px;
}

.pur-review__identity {
    margin: 0 0 8px;
    font-size: 14px;
    overflow-wrap: anywhere;
}

.pur-review__steps {
    display: flex;
    flex-wrap: wrap;
    gap: 4px 16px;
    margin: 0 0 8px;
    padding: 0;
    list-style: none;
    counter-reset: pur-step;
    font-size: 12px;
    color: var(--muted);
}

.pur-review__steps li {
    counter-increment: pur-step;
}

.pur-review__steps li::before {
    content: counter(pur-step) ". ";
}

.pur-review__steps li[aria-current="step"] {
    color: var(--ink);
    font-weight: 600;
}

.pur-review__notice {
    margin: 0 0 8px;
    padding: 8px 10px;
    border-radius: var(--radius-sm);
    background: var(--accent-soft);
    font-size: 13px;
}

.pur-review__honesty {
    margin: 0 0 12px;
    font-size: 12px;
    color: var(--muted);
}

.pur-review__switch,
.pur-review__jump,
.pur-review__peek {
    display: none;
}

.pur-review__panes {
    display: grid;
    grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
    gap: 16px;
    align-items: start;
}

.pur-review__pane {
    min-width: 0;
}

.pur-review__pane:focus-visible,
.pur-review__tab:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

.pur-review__pane-title {
    margin: 0 0 8px;
    font-size: 14px;
}

.pur-doc__stage {
    position: relative;
    min-height: 240px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--paper);
}

.pur-doc__frame {
    display: block;
    width: 100%;
    height: calc(100vh - 220px);
    min-height: 480px;
    border: 0;
    border-radius: var(--radius-sm);
}

/* Only the script can take the loading line away, so only a page the script
   enhanced shows it, and only while the stage says "loading" (Codex early
   review 6). Without the script the document and its links stand alone. */
.pur-doc__loading {
    position: absolute;
    inset: 0;
    display: none;
    align-items: center;
    justify-content: center;
    margin: 0;
    font-size: 13px;
    color: var(--muted);
    pointer-events: none;
}

.pur-review.is-enhanced .pur-doc__stage[data-state="loading"] .pur-doc__loading {
    display: flex;
}

.pur-doc__image {
    position: relative;
    display: block;
    max-width: 100%;
    height: auto;
    margin: 0 auto;
}

.pur-doc__stage.is-zoomed {
    max-height: calc(100vh - 220px);
    overflow: auto;
}

.pur-doc__stage.is-zoomed .pur-doc__image {
    max-width: none;
}

.pur-doc__name,
.pur-doc__hint,
.pur-doc__actions,
.pur-doc__state,
.pur-doc__tools {
    margin: 6px 0 0;
    font-size: 12px;
    overflow-wrap: anywhere;
}

.pur-doc__state,
.pur-doc__hint {
    color: var(--muted);
}

.pur-doc__back {
    margin: 10px 0 0;
}

.pur-review__errors {
    margin: 0 0 12px;
    padding: 10px 12px;
    border: 1px solid var(--danger);
    border-radius: var(--radius-sm);
    background: var(--danger-soft);
    font-size: 13px;
}

.pur-review__errors p {
    margin: 0 0 4px;
}

.pur-review__errors ul {
    margin: 6px 0;
    padding-left: 20px;
}

.pur-review__group {
    min-width: 0;
    margin: 0 0 12px;
    padding: 0;
    border: 0;
}

.pur-review__group legend,
.pur-review__group > summary {
    margin-bottom: 6px;
    font-size: 13px;
    font-weight: 600;
}

.pur-review__group > summary {
    cursor: pointer;
}

.pur-review__grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
    gap: 10px;
}

.pur-review__grid--accounts {
    margin-top: 10px;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}

.pur-field {
    display: flex;
    flex-direction: column;
    gap: 3px;
    min-width: 0;
    font-size: 13px;
}

.pur-field .app-input {
    width: 100%;
    min-height: 40px;
}

.pur-field__hint {
    font-size: 12px;
    color: var(--muted);
}

.pur-field__suggest {
    font-size: 12px;
    color: var(--signal);
}

.pur-field__error {
    font-size: 12px;
    font-weight: 600;
    color: var(--danger);
}

.pur-field--invalid .app-input {
    border-color: var(--danger);
}

.pur-account__search {
    display: flex;
    flex-direction: column;
    gap: 3px;
    margin-bottom: 4px;
    font-size: 12px;
}

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

.pur-review__actions {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    align-items: center;
    margin: 12px 0 6px;
}

.pur-scroll {
    max-width: 100%;
    overflow-x: auto;
}

.pur-review__lines {
    width: 100%;
    border-collapse: collapse;
    font-size: 13px;
}

.pur-review__lines caption {
    margin-bottom: 6px;
    text-align: left;
    font-weight: 600;
}

.pur-review__lines th,
.pur-review__lines td {
    padding: 6px 8px;
    border-bottom: 1px solid var(--line);
    text-align: left;
    vertical-align: top;
}

.pur-review__lines thead th {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--muted);
    border-bottom-width: 2px;
}

.pur-review__lines .pur-num,
.pur-num {
    text-align: right;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}

.pur-review__facts {
    display: grid;
    grid-template-columns: max-content minmax(0, 1fr);
    gap: 4px 12px;
    margin: 12px 0;
    font-size: 13px;
}

.pur-review__facts dt {
    color: var(--muted);
}

.pur-review__facts dd {
    margin: 0;
    overflow-wrap: anywhere;
}

.pur-result {
    padding: 16px;
    margin-bottom: 16px;
}

.pur-result__actions,
.pur-result__next {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    align-items: center;
}

.pur-pending ul {
    margin: 6px 0 0;
    padding-left: 20px;
    font-size: 13px;
}

.pur-review__tab {
    flex: 1 1 0;
    min-height: 44px;
    padding: 8px 12px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--paper);
    color: var(--ink);
    font: inherit;
    font-weight: 600;
    cursor: pointer;
}

.pur-review__tab[aria-pressed="true"] {
    border-color: var(--accent);
    background: var(--accent);
    color: #fff;
}

@media (max-width: 1100px) {
    .pur-review__panes {
        grid-template-columns: minmax(0, 1fr);
    }

    .pur-doc__frame {
        height: 70vh;
        min-height: 360px;
    }

    .pur-review__jump {
        display: block;
        margin: 0 0 8px;
        font-size: 13px;
    }

    .pur-review.is-enhanced .pur-review__jump {
        display: none;
    }

    .pur-review.is-enhanced .pur-review__switch {
        display: flex;
        gap: 6px;
        margin: 0 0 8px;
    }

    .pur-review.is-enhanced .pur-review__pane.is-inactive {
        display: none;
    }

    .pur-review__peek {
        display: block;
        margin: 10px 0 0;
    }

    .pur-review__facts {
        grid-template-columns: minmax(0, 1fr);
    }

    .pur-review__facts dd {
        margin-bottom: 6px;
    }
}

/* A wide screen, where the invoice and its fields sit side by side (Codex
   early review 6): beside a shown document the field pane is one frame high and
   scrolls on its own, so reaching the accounts never scrolls the invoice away,
   and an image scrolls inside a box of the same height. Without :has() support
   the page simply scrolls as before. */
@media (min-width: 1101px) {
    .pur-review__panes:has(.pur-doc__stage) .pur-review__form {
        max-height: max(480px, calc(100vh - 220px));
        overflow-y: auto;
        padding: 2px 6px 2px 2px;
    }

    .pur-doc__stage[data-pur-doc="image"] {
        max-height: max(480px, calc(100vh - 220px));
        overflow: auto;
    }
}
