/* Hamnkapten customer web app: SJÖKORT component recipes, mobile-first.
   Tokens come from ui-tokens/generated/theme.css; nothing in here hardcodes a
   colour. The class vocabulary is shared with the web admin so the two surfaces
   read as one system; customer-only pieces (taken time, booking cards,
   feedback form) are appended. Constitution reminders: mono = every numeral;
   signal magenta = non-text, budgeted; motion is event-driven and plays once. */

/* 1. Fonts: the three SJÖKORT voices. Plex Sans = UI, Plex Mono = soundings
   (every time/code numeral), Besley = display at the enumerated places only.
   All SIL OFL, self-hosted. */
@font-face { font-family: 'IBM Plex Sans'; font-style: normal; font-weight: 400; font-display: swap; src: url('/fonts/ibm-plex-sans-latin-400-normal.woff2') format('woff2'); }
@font-face { font-family: 'IBM Plex Sans'; font-style: normal; font-weight: 500; font-display: swap; src: url('/fonts/ibm-plex-sans-latin-500-normal.woff2') format('woff2'); }
@font-face { font-family: 'IBM Plex Sans'; font-style: normal; font-weight: 600; font-display: swap; src: url('/fonts/ibm-plex-sans-latin-600-normal.woff2') format('woff2'); }
@font-face { font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 400; font-display: swap; src: url('/fonts/ibm-plex-mono-latin-400-normal.woff2') format('woff2'); }
@font-face { font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 500; font-display: swap; src: url('/fonts/ibm-plex-mono-latin-500-normal.woff2') format('woff2'); }
@font-face { font-family: 'Besley'; font-style: normal; font-weight: 500; font-display: swap; src: url('/fonts/besley-latin-500-normal.woff2') format('woff2'); }
@font-face { font-family: 'Besley'; font-style: normal; font-weight: 600; font-display: swap; src: url('/fonts/besley-latin-600-normal.woff2') format('woff2'); }

/* 2. Base */
*, *::before, *::after { box-sizing: border-box; }
body {
  margin: 0;
  background: var(--bg-base);
  color: var(--fg-primary);
  font-family: var(--font-ui);
  font-size: var(--text-base);
  line-height: var(--leading-body);
  -webkit-font-smoothing: antialiased;
}
h1, h2 { font-weight: var(--weight-display); line-height: var(--leading-heading); margin: 0 0 var(--space-2); }
h1 { font-size: var(--text-2xl); font-family: var(--font-display); }
h2 { font-size: var(--text-lg); margin-top: var(--space-3); }
p { margin: 0 0 var(--space-2); }
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
/* Keyboard users land on the content, not on seven rail links: the link is
   off-screen until it has focus, then sits over the top bar. */
.skip-link {
  position: absolute; left: var(--space-2); top: -100px; z-index: 100;
  padding: var(--space-1) var(--space-2);
  background: var(--bg-elevated); color: var(--fg-primary);
  border: var(--border-width-hairline) solid var(--border-strong); border-radius: var(--radius-sm);
}
.skip-link:focus-visible { top: var(--space-2); }
/* `<main>` takes focus from the skip link and after each change of page (issue #223).
   It is a landmark, not a control, so it draws no ring; the page's own controls do. */
.app-main:focus { outline: none; }
.mono, .booking-time, .booking-code-value, .avail-slot-time { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }

/* Reduced-motion contract: everything collapses to a short fade. Reduce when
   EITHER the OS prefers reduced motion OR the in-app toggle sets
   `[data-reduce-motion]` on the root. The two triggers cannot share one selector
   (one is an @media query, one a plain attribute selector), so the block is
   repeated once. The timing is factored into the `--motion-reduced` token. */
@media (prefers-reduced-motion: reduce) {
  * {
    transition-property: opacity !important;
    transition-duration: var(--motion-reduced) !important;
    animation: none !important;
  }
}
[data-reduce-motion] * {
  transition-property: opacity !important;
  transition-duration: var(--motion-reduced) !important;
  animation: none !important;
}

/* 3. Frame */
.app-frame { display: flex; flex-direction: column; min-height: 100vh; }
.app-frame .app-shell { flex: 1; min-height: 0; }
/* The rail's column is painted by the shell, as in the admin app: a 208px band
   of elevated fill closed by a 1px border, the full height of the page. The rail
   itself is only as tall as its items and sticks to the viewport top, so the
   menu stays in reach while the page scrolls (Captain, 2026-09-02). The ≤640
   bottom bar drops the band. */
/* `--rail-w` is the one width the shell band and the rail both read. The folded
   value is set below from the `data-rail` root flag. */
:root { --rail-w: 208px; }
.app-shell {
  display: flex; align-items: stretch;
  background: linear-gradient(to right,
    var(--bg-elevated) calc(var(--rail-w) - 1px),
    var(--border-default) calc(var(--rail-w) - 1px) var(--rail-w),
    transparent var(--rail-w));
}
.app-nav {
  --nav-item-h: 40px;
  position: sticky; top: 0; align-self: flex-start;
  max-height: 100vh; overflow-y: auto;
  display: flex; flex-direction: column; gap: var(--space-0-5);
  width: var(--rail-w); flex-shrink: 0;
  padding: var(--space-3) var(--space-2);
  background: var(--bg-elevated);
  border-right: 1px solid var(--border-default);
}
.app-nav a {
  display: flex; align-items: center; gap: var(--space-1);
  height: var(--nav-item-h); box-sizing: border-box;
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-sm);
  color: var(--fg-secondary); font-weight: var(--weight-ui);
  transition: background var(--motion-fast) var(--ease-standard), color var(--motion-fast) var(--ease-standard);
  animation: rise-in var(--motion-base) var(--ease-standard) backwards;
}
.app-nav a:hover { background: var(--bg-sunken); color: var(--fg-primary); text-decoration: none; }
.app-nav a[aria-current="page"] { color: var(--accent); background: var(--bg-sunken); }
.app-nav a:nth-of-type(1) { animation-delay: 30ms; }
.app-nav a:nth-of-type(2) { animation-delay: 60ms; }
.app-nav a:nth-of-type(3) { animation-delay: 90ms; }
/* Travelling route-line marker: one 3px signal bar that slides to the active
   item, set inline from the active route. Reduced motion drops the transition. */
.nav-marker {
  position: absolute; left: var(--space-2); top: var(--space-3);
  width: 3px; height: var(--nav-item-h);
  border-radius: 1.5px; background: var(--signal);
  transition: transform var(--motion-base) ease-in-out;
  pointer-events: none;
}
.app-main {
  flex: 1; min-width: 0; max-width: 900px; padding: var(--space-4);
  animation: rise-in var(--motion-base) var(--ease-standard) backwards;
}
/* The calendar is the one page without the measure: its rows spread the hours
   across the width and use all of it. Still left-aligned, same padding. */
.app-main.wide { max-width: none; }
.boot-status, .boot-error { padding: var(--space-4); color: var(--fg-secondary); }
.boot-error { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-2); }
@keyframes rise-in { from { opacity: 0; transform: translateY(12px); } }

/* 3a. Relingen iconography, the admin rail's: currentColor base + magenta
   detail overlay that lights on the active item. */
.nav-icon { position: relative; width: 20px; height: 20px; flex-shrink: 0; }
.nav-icon svg { width: 20px; height: 20px; display: block; }
.nav-icon-base, .nav-icon-detail { position: absolute; inset: 0; }
.nav-icon-detail { color: var(--signal); opacity: 0; transition: opacity var(--motion-fast) var(--ease-standard); }
.app-nav a[aria-current="page"] .nav-icon-detail { opacity: 1; }

/* 3b. Relingen folded (FR-17, web half), the admin stylesheet's twin: the
   `data-rail="collapsed"` root flag narrows `--rail-w` to a 56px icon column,
   labels leave the flow, the marker keeps its item height, and the fold control
   at the foot of the rail turns its chevron. Wide rail only; the ≤640 bottom bar
   hides the control. */
.nav-toggle {
  display: flex; align-items: center; gap: var(--space-1);
  height: var(--nav-item-h); width: 100%; box-sizing: border-box;
  margin-top: var(--space-2); padding: var(--space-1) var(--space-2);
  border: 0; border-radius: var(--radius-sm); background: transparent;
  color: var(--fg-secondary); font-weight: var(--weight-ui); text-align: left;
}
.nav-toggle:hover { background: var(--bg-sunken); color: var(--fg-primary); }
.nav-toggle .nav-icon svg { transition: transform var(--motion-base) var(--ease-standard); }
/* "Fäll ihop relingen" is about 126 px at 16 px, 11 more than the 115 the item leaves
   it, so it keeps one line and runs into the item's right padding, as the desktop's
   label does. It wrapped onto two lines before. */
.nav-toggle .nav-label { white-space: nowrap; }
@media (min-width: 641px) {
  :root[data-rail="collapsed"] { --rail-w: 56px; }
  :root[data-rail="collapsed"] .app-nav { padding-inline: var(--space-1); }
  :root[data-rail="collapsed"] .app-nav a,
  :root[data-rail="collapsed"] .nav-toggle { justify-content: center; padding-inline: 0; }
  :root[data-rail="collapsed"] .nav-label { display: none; }
  :root[data-rail="collapsed"] .nav-marker { left: var(--space-1); }
  :root[data-rail="collapsed"] .nav-toggle .nav-icon svg { transform: rotate(180deg); }
}

/* 4. Top bar */
.topbar {
  display: flex; align-items: center; gap: var(--space-2);
  padding: var(--space-1) var(--space-2);
  background: var(--bg-elevated);
  border-bottom: 1px solid var(--border-default);
}
.topbar-brand { display: flex; align-items: center; gap: var(--space-1); color: var(--accent); }
.topbar-brand:hover { text-decoration: none; }
.topbar-mark svg { width: 22px; height: 22px; display: block; }
.topbar-wordmark { font-family: var(--font-display); font-weight: var(--weight-display); font-size: var(--text-lg); color: var(--fg-primary); }
.topbar-spacer { flex: 1; }
.topbar select { width: auto; margin-top: 0; }

/* 5. Controls: 4 px radius, borders over shadows, fast hover micro. The Swish
   payment link (FR-35) is an anchor, so it takes the button look by name, and so does
   any link styled as a secondary button, such as Konto's "Tyck till om appen". */
button, .swish-pay, a.btn-secondary {
  font: inherit; font-weight: var(--weight-ui); cursor: pointer;
  padding: var(--space-1) var(--space-3);
  border: 1px solid transparent; border-radius: var(--radius-sm);
  background: var(--accent); color: var(--fg-on-accent);
  transition: background var(--motion-fast) var(--ease-standard), border-color var(--motion-fast) var(--ease-standard);
}
button:hover, .swish-pay:hover { background: var(--accent-hover); }
button:active { transform: scale(0.99); }
button:disabled, input:disabled, select:disabled, textarea:disabled { opacity: .6; cursor: not-allowed; }
/* `a.btn-secondary` is named beside the class so a link outranks the filled look above. */
.btn-secondary, a.btn-secondary { background: transparent; color: var(--accent); border-color: var(--border-strong); }
.btn-secondary:hover { background: var(--bg-sunken); }
a.btn-secondary { display: inline-flex; align-items: center; text-decoration: none; }
.btn-danger { background: var(--danger); color: var(--fg-on-accent); }

label { display: block; font-weight: var(--weight-ui); font-size: var(--text-sm); color: var(--fg-secondary); margin-bottom: var(--space-2); }
/* A field whose label has a help hint beside it (issue #222). The hint sits outside the
   label, so the label names only its field; the box takes the plain label's voice and
   spacing, so the field looks as it did when it sat inside the label. */
.field { display: block; font-weight: var(--weight-ui); font-size: var(--text-sm); color: var(--fg-secondary); margin-bottom: var(--space-2); }
.field-head { display: inline-flex; align-items: center; }
.field-head label { display: inline; margin: 0; }
input, select, textarea {
  font: inherit; width: 100%; margin-top: var(--space-0-5);
  padding: var(--space-1) var(--space-2);
  background: var(--bg-elevated); color: var(--fg-primary);
  border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
}
input:focus-visible, select:focus-visible, textarea:focus-visible {
  outline: 2px solid var(--focus-ring); outline-offset: 2px; border-color: var(--focus-ring);
}
.form-error { color: var(--danger); font-size: var(--text-sm); }

/* 5b. Six-box OTP input that mirrors the admin SegmentedCode look. */
.totp-field { display: flex; flex-direction: column; gap: var(--space-0-5); }
.totp-label { display: inline-flex; align-items: center; }
.totp-boxes { display: flex; gap: var(--space-1); margin-top: var(--space-0-5); }
.totp-box {
  width: 40px; height: 48px; flex: 0 0 auto; margin-top: 0;
  padding: 0; text-align: center;
  font-size: var(--text-xl); font-variant-numeric: tabular-nums;
  background: var(--bg-base); color: var(--fg-primary);
  border: 1px solid var(--border-strong); border-radius: var(--radius-md);
}
.totp-box:focus-visible {
  outline: none; border-width: 2px; border-color: var(--focus-ring);
  margin: -1px; padding: 0;
}

/* 6. Neatline register-ticks: four L-shaped corner strokes on cards/modals. */
.neatline { position: relative; }
.neatline::before {
  content: ""; position: absolute; inset: 6px; pointer-events: none;
  --tick: color-mix(in srgb, var(--fg-secondary) 35%, transparent);
  background:
    linear-gradient(var(--tick), var(--tick)) 0 0 / 6px 1px,
    linear-gradient(var(--tick), var(--tick)) 0 0 / 1px 6px,
    linear-gradient(var(--tick), var(--tick)) 100% 0 / 6px 1px,
    linear-gradient(var(--tick), var(--tick)) 100% 0 / 1px 6px,
    linear-gradient(var(--tick), var(--tick)) 0 100% / 6px 1px,
    linear-gradient(var(--tick), var(--tick)) 0 100% / 1px 6px,
    linear-gradient(var(--tick), var(--tick)) 100% 100% / 6px 1px,
    linear-gradient(var(--tick), var(--tick)) 100% 100% / 1px 6px;
  background-repeat: no-repeat;
}

/* 7. Cards + sections */
.settings-section {
  padding: var(--space-3); margin-bottom: var(--space-3);
  background: var(--bg-elevated); border: 1px solid var(--border-default);
  border-radius: var(--radius-md); box-shadow: var(--shadow-sm);
}
.settings-values { margin: 0; }
.settings-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1) var(--space-3); padding: var(--space-1) 0; }
.settings-row dt { color: var(--fg-secondary); font-size: var(--text-sm); min-width: 160px; }
.settings-row dd { margin: 0; }
.settings-check { display: flex; align-items: center; gap: var(--space-1); margin-bottom: 0; }
.settings-checkbox { width: auto; margin-top: 0; accent-color: var(--accent); }
/* Konto's two actions side by side, each a 44 px touch target (DESIGN.md, Direction). */
.account-feedback-link, .account-logout { min-height: 44px; margin-right: var(--space-1); vertical-align: middle; }
.customer-gdpr .btn-danger { margin-top: var(--space-1); }

/* 8. Badges + status inks: status is never colour alone. */
.badge {
  display: inline-flex; align-items: center; gap: var(--space-0-5);
  padding: 2px var(--space-1); border-radius: var(--radius-pill);
  font-size: var(--text-xs); font-weight: var(--weight-ui);
  background: var(--bg-sunken); color: var(--fg-secondary);
}
.badge.active { background: var(--status-paid-tint); color: var(--status-paid); }
.booking-status { font-size: var(--text-sm); font-weight: var(--weight-ui); }
.booking-status.paid { color: var(--status-paid); }
.booking-status.unpaid { color: var(--status-unpaid); }
.booking-status.held { color: var(--status-held); }
.booking-status.cancelled { color: var(--status-cancelled); text-decoration: line-through; }

/* 9. Mina bokningar: booking cards */
.booking-list { list-style: none; margin: 0; padding: 0; }
.booking-cards { display: flex; flex-direction: column; gap: var(--space-2); }
/* Ended sessions fold under "Tidigare" below the upcoming ones (#229). The summary
   reads as a section heading and keeps a 44px tap height. */
.bookings-past { margin-top: var(--space-3); }
.bookings-past > summary {
  line-height: 44px; cursor: pointer;
  font-size: var(--text-lg); font-weight: var(--weight-display); color: var(--fg-primary);
}
.bookings-past > summary::marker { color: var(--fg-secondary); }
.bookings-past[open] > summary { margin-bottom: var(--space-2); }
.booking-card {
  display: flex; flex-direction: column; gap: var(--space-1);
  padding: var(--space-3);
  background: var(--bg-elevated); border: 1px solid var(--border-default);
  border-radius: var(--radius-md); box-shadow: var(--shadow-sm);
}
.booking-card-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-2); }
.booking-time { color: var(--fg-primary); font-weight: var(--weight-ui); }
.booking-card-meta { display: flex; gap: var(--space-1); align-items: center; }
.booking-price { color: var(--fg-muted); font-size: var(--text-sm); }
.booking-code-block {
  display: flex; flex-direction: column; gap: var(--space-0-5);
  padding: var(--space-2); margin-top: var(--space-0-5);
  background: var(--bg-sunken); border-radius: var(--radius-sm);
}
.booking-code-label { display: inline-flex; align-items: center; color: var(--fg-secondary); font-size: var(--text-sm); }
/* Muted ink on the sunken block misses AA at 12 px (issue #244), so the hint takes the
   label's ink here. It sits before section 11, so the hint's hover colour still wins. */
.booking-code-block .help-hint-button { color: var(--fg-secondary); border-color: var(--fg-secondary); }
/* The code is a sounding: mono, large, primary ink, the value the customer copies. */
.booking-code-value { font-size: var(--text-xl); font-weight: var(--weight-ui); letter-spacing: 0.08em; color: var(--fg-primary); }
.booking-code-swish { color: var(--fg-secondary); font-size: var(--text-sm); }
/* FR-35: the prefilled Swish link and its QR side by side, wrapping under 400px. The QR
   box stays white in every theme: an inverted code is what scanners fail on. */
.swish-pay-block { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); margin-top: var(--space-1); }
.swish-qr { flex: 0 0 auto; width: 112px; height: 112px; padding: var(--space-0-5); background: #fff; border-radius: var(--radius-sm); box-sizing: border-box; }
.swish-qr svg { display: block; width: 100%; height: 100%; }
.swish-pay-actions { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-1); flex: 1 1 160px; }
.swish-pay { display: inline-flex; align-items: center; text-decoration: none; }
.swish-pay-hint { color: var(--fg-secondary); font-size: var(--text-sm); }
.hold-countdown { margin: var(--space-1) 0 0; color: var(--accent); font-weight: var(--weight-ui); }
/* FR-39: the door code, shown only inside the paid session window, in the code's own voice. */
.door-code { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-1); margin-top: var(--space-1); padding: var(--space-1) var(--space-2); background: var(--bg-sunken); border-radius: var(--radius-sm); }
.door-code-label { color: var(--fg-secondary); font-size: var(--text-sm); }
.door-code-value { font-size: var(--text-xl); font-weight: var(--weight-ui); letter-spacing: 0.12em; color: var(--fg-primary); }
.door-code-hint { flex-basis: 100%; color: var(--fg-secondary); font-size: var(--text-xs); }

/* 10. Tillgänglighet: the availability chart. */
.cal-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); margin-bottom: var(--space-2); }
.cal-range { font-weight: var(--weight-ui); }
.cal-viewtoggle { margin-left: auto; display: flex; gap: var(--space-0-5); }
.cal-viewtoggle button { background: transparent; color: var(--accent); border-color: var(--border-strong); }
.cal-viewtoggle button:hover { background: var(--bg-sunken); }
.cal-viewtoggle button.active { background: var(--accent); color: var(--fg-on-accent); border-color: var(--accent); }
.cal-grid { display: grid; grid-template-columns: auto 1fr; gap: var(--space-0-5); padding: var(--space-0-5); background: var(--bg-sunken); border: 1px solid var(--border-default); border-radius: var(--radius-md); }

/* 10b. Taken time (G-1): another member's booking is a solid grey block placed
   by its inline style, its time range the only text and nothing traceable to
   the booking or its holder. It was hatched until 2026-09-28; hatching now
   means time that has passed, so taken time went solid. High contrast keeps
   the 4px edge and the border. The member's own booking is a button in its
   status colour. The block's text takes the block's ink, so every pair is one
   the AA gate checks. */
.avail-slot {
  position: absolute; z-index: 1;
  border: 1px solid var(--border-default); border-left: 4px solid var(--border-strong);
  border-radius: var(--radius-block);
  padding: 2px 4px; overflow: hidden; font: inherit; line-height: 1.2; text-align: left;
  color: var(--fg-primary); background: var(--taken-tint);
}
.avail-slot.own { cursor: pointer; background: var(--status-held-tint); border-color: var(--status-held); color: var(--status-held); }
.avail-slot.own.unpaid { background: var(--status-unpaid-tint); border-color: var(--status-unpaid); color: var(--status-unpaid); }
.avail-slot.own.paid { background: var(--status-paid-tint); border-color: var(--status-paid); color: var(--status-paid); }
.avail-slot-time { font-size: var(--text-xs); }
.avail-slot-own { font-weight: var(--weight-display); }
/* On a row a block is short and often narrow: each line keeps to one line and
   cuts with an ellipsis. On a phone it shows only its fill and marks; its
   aria-label names the time. */
.cal-track :is(.avail-slot-time, .avail-slot-own) { display: block; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
@container (max-width: 520px) {
  .cal-track :is(.avail-slot-time, .avail-slot-own) { display: none; }
  .cal-track .avail-slot { padding: 0; }
}
/* The phone bar: each row also carries its pieces again as plain boxes with the same
   classes, hidden from every reader. Only a phone shows it, in place of the buttons. */
.cal-bar { display: none; position: absolute; inset: 0; pointer-events: none; }

/* 11. Help-hints */
.help-hint { position: relative; display: inline-flex; vertical-align: middle; margin-left: var(--space-0-5); }
.help-hint-button {
  width: 16px; height: 16px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  background: transparent; color: var(--fg-muted);
  border: 1px solid var(--fg-muted); border-radius: var(--radius-pill);
  font-size: var(--text-xs); font-weight: var(--weight-ui); line-height: 1;
  transition: color var(--motion-fast) var(--ease-standard), border-color var(--motion-fast) var(--ease-standard);
}
.help-hint-button:hover { background: transparent; color: var(--accent); border-color: var(--accent); }
.help-hint-text {
  position: absolute; left: 0; right: auto; top: calc(100% + var(--space-0-5)); z-index: 10;
  width: max-content; max-width: min(240px, calc(100vw - var(--space-4)));
  padding: var(--space-1) var(--space-2);
  background: var(--bg-elevated); color: var(--fg-primary);
  border: 1px solid var(--border-default); border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  font-size: var(--text-sm); font-weight: var(--weight-body);
  opacity: 0; visibility: hidden; transition: opacity var(--motion-fast) var(--ease-standard);
}
.help-hint:hover .help-hint-text, .help-hint:focus-within .help-hint-text { opacity: 1; visibility: visible; }

/* 12. Feedback form + prompt */
.feedback-rating { display: flex; align-items: center; gap: var(--space-2); border: none; padding: 0; margin: 0; }
.feedback-rating legend { padding: 0; color: var(--fg-secondary); font-size: var(--text-sm); }
.feedback-star { display: inline-flex; align-items: center; gap: 2px; margin: 0; font-weight: var(--weight-ui); }
.feedback-star input { width: auto; margin: 0; accent-color: var(--accent); }
.feedback-body { min-height: 96px; resize: vertical; }
/* The prompt reuses the onboarding modal shell (`.onb` + `.loggkort-scrim`). */
.onb {
  position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
  z-index: 60; width: min(420px, 92vw);
  background: var(--bg-elevated); border: 1px solid var(--border-strong);
  border-radius: var(--radius-md); box-shadow: var(--shadow-md);
  padding: var(--space-3);
  animation: rise-in var(--motion-base) var(--ease-standard) backwards;
}
.onb-title { font-family: var(--font-display); margin: 0 0 var(--space-1); }
.onb-price { margin: 0; color: var(--fg-muted); }
.onb-actions { display: flex; align-items: center; gap: var(--space-1); margin-top: var(--space-3); flex-wrap: wrap; }
.loggkort-scrim {
  position: fixed; inset: 0; z-index: 40;
  background: rgba(22, 48, 62, 0.32);
  animation: scrim-in var(--motion-base) var(--ease-standard) backwards;
}
@keyframes scrim-in { from { opacity: 0; } }
.loggkort-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1); margin-top: var(--space-1); }
.loggkort-note { color: var(--fg-secondary); font-size: var(--text-sm); margin: 0; flex-basis: 100%; }

/* 13. Signalen, the toast: drawn for both web apps from
   crates/ui-tokens/css/signalen.css, which build.rs appends to theme.css. */

/* 14. Login lockup + card. */
.login-brand {
  display: flex; align-items: center; justify-content: center; gap: var(--space-1);
  margin-top: var(--space-8); color: var(--accent);
}
.login-mark svg { width: 28px; height: 28px; display: block; }
.login-wordmark { font-family: var(--font-display); font-weight: var(--weight-display); font-size: var(--text-xl); color: var(--fg-primary); }
.login-title { margin-bottom: 0; }
.login-form {
  display: flex; flex-direction: column; gap: var(--space-2); max-width: 360px;
  margin: var(--space-3) auto 0;
  padding: var(--space-4);
  background: var(--bg-elevated); border: 1px solid var(--border-default);
  border-radius: var(--radius-md); box-shadow: var(--shadow-md);
  animation: rise-in var(--motion-slow) var(--ease-standard) backwards;
}
/* "Ändra nummer" is drawn at the height of the buttons above it, which is under
   44 px, so an invisible layer gives it a touch area of at least 44 px
   (DESIGN.md, 2026-10-09). The form's gap leaves room for it. */
.login-back { position: relative; }
.login-back::after {
  content: ""; position: absolute; left: 0; right: 0; top: 50%;
  height: max(100%, 44px); transform: translateY(-50%);
}

/* 15. Elevation-as-light (dark theme): a 1px lighter top edge plus a faint glow
   painted inside each surface. The two tokens are fully transparent in light and
   high-contrast, so this rule is inert there. Signalen is not on the list: in dark
   it is a cream card, lit already. */
.settings-section, .booking-card, .login-form, .help-hint-text, .onb {
  background-image: linear-gradient(
    180deg,
    var(--elevation-edge) 0,
    var(--elevation-edge) 1px,
    var(--elevation-glow) 1px,
    transparent 48px
  );
}

/* 16. Responsive: mobile-first is this surface's primary device. The rail
   collapses to a pinned bottom bar; the week grid linearises to an agenda stack. */
@media (max-width: 768px) {
  /* Help-hint popup can't stay on-screen for every trigger position at narrow
     widths, so re-pin it as a fixed, viewport-clamped banner. */
  .help-hint-text {
    position: fixed; left: var(--space-2); right: var(--space-2);
    top: auto; bottom: calc(var(--nav-bar-h, 0px) + var(--space-2));
    width: auto; max-width: none;
  }
}
@media (max-width: 640px) {
  /* 57px holds the 1px top border, the bar's padding and the item's, both
     space-0-5, and the stack: a 20px icon, the 2px gap and one 12px label line
     at the body line height, 18px. At 56px the label lost a pixel and its
     descenders, as the admin bar's did. */
  :root { --nav-bar-h: 57px; }
  .topbar-wordmark { display: none; }
  .topbar { gap: var(--space-1); padding: var(--space-1); flex-wrap: wrap; }
  .topbar select { max-width: 8rem; flex-shrink: 0; }
  .app-shell { flex-direction: column; background: none; }
  /* `top: auto` and `align-self: auto` undo the wide rail's sticky placement,
     or the bar would pin to the top edge instead of the bottom. */
  .app-nav {
    position: fixed; left: 0; right: 0; bottom: 0; top: auto; z-index: 30;
    align-self: auto; max-height: none; overflow: visible;
    flex-direction: row; width: auto; height: var(--nav-bar-h); gap: var(--space-0-5);
    border-right: none; border-top: 1px solid var(--border-default);
    padding: var(--space-0-5); background: var(--bg-elevated);
  }
  .app-nav a {
    flex: 1; height: auto; min-width: 0;
    flex-direction: column; justify-content: center; gap: 2px;
    padding: var(--space-0-5);
  }
  .app-nav a .nav-label { font-size: var(--text-xs); }
  /* A label must never widen its item, so it ellipsises inside the item's flex
     share rather than overrunning its neighbour. The member's bar has three
     items, so every label shows at every width, ruled on 2026-10-14 (#246);
     the admin bar's icons-only idiom is for its seven. */
  .app-nav a .nav-label {
    flex-shrink: 0; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  /* Six 40px code boxes plus the card's padding are 344px; under 360 the boxes
     drop to 36px so the row fits a 320px screen. */
  @media (max-width: 360px) {
    .totp-boxes { gap: 6px; }
    .totp-box { width: 36px; height: 44px; }
  }
  .nav-marker, .nav-toggle { display: none; }
  .app-nav a:hover { background: transparent; }
  .app-nav a[aria-current="page"] { background: transparent; box-shadow: inset 0 2px 0 var(--signal); }
  .app-main { padding: var(--space-2); padding-bottom: calc(var(--nav-bar-h) + var(--space-2)); }
  .cal-toolbar { flex-wrap: wrap; gap: var(--space-1); }
  .cal-viewtoggle { margin-left: 0; flex-wrap: wrap; }
  .cal-range { min-width: 0; }
  /* Members on a phone: each day's row becomes a thin overview bar that is not a tap
     target, and the day's free stretches are listed under it (DESIGN.md, Direction).
     The row's buttons give way to the bar's plain copies, so a keyboard or a screen
     reader reaches each stretch once, through the list. The bar is too thin for the
     stamp; the list carries the member's own, stamp and status included. */
  /* A label that books is a 44 px touch target; every row takes that height, so a full
     day's row is as tall as the rest. */
  .cal-row:not(.cal-tide-row) { --cal-rowh: 16px; min-height: 44px; }
  .cal-row .cal-track > :is(.cal-free, .cal-short, .avail-slot) { display: none; }
  .cal-bar { display: block; }
  .cal-daylist { display: block; margin-bottom: var(--space-1); }
}

/* 11. Sea-state banner, ported from the admin calendar (crates/app-admin-web/
   styles/app.css); the water itself is painted from ui-tokens, so the three
   clients draw one sea. */
/* Sea-state: the density-tier caption above a decorative water strip.

   The strip is a canvas, and nothing here draws on it. `seaband` builds the
   geometry, ui-tokens' `sea_canvas` finds the canvas by its `data-sea`
   attribute and paints it frame by frame, and the desktop paints the same scene
   through its own rasteriser. What this file settles is the two things the
   painter reads back off the element.

   Nothing here decides the water's colours, and that is deliberate. The banner
   carries one of sixteen palettes, four hours by four seasons, chosen by the
   clock and held in ui-tokens; the theme the viewer picked decides the caption
   above the strip and nothing inside it. So a December dusk warms the water
   toward amber on a page that is still the user's light or dark, and no text
   anywhere shifts by a shade. It is safe only because the strip is decorative,
   `aria-hidden`, carries no text, and is dropped outright under high contrast.

   Reduced motion: the §2 `* { animation: none }` contract cannot reach inside a
   canvas, so the painter honours the preference itself, from the media query and
   the `[data-reduce-motion]` toggle alike. It paints `seaband`'s chosen still
   frame, the moment in the loop whose fleet is best spread across the band,
   rather than pausing the water wherever it happened to be.

   High contrast: the strip is dropped (water texture off, per spec); the
   caption above it still shows. The painter reads a zero-width canvas and skips
   it, so a hidden strip costs nothing per frame. */
.sea-caption { text-align: center; margin-bottom: var(--space-1); }
.sea-title {
  font-family: var(--font-display); font-weight: var(--weight-display);
  font-size: var(--text-lg); margin: 0; padding-top: var(--space-2);
  animation: rise-in var(--motion-slow) var(--ease-standard) backwards;
}
.sea-text {
  color: var(--fg-secondary); margin: var(--space-0-5) 0 0;
  animation: rise-in var(--motion-slow) var(--ease-standard) backwards;
}

.sea-banner {
  position: relative; height: var(--sea-strip-h); overflow: hidden;
  margin-bottom: var(--space-2);
}
:root[data-theme="high-contrast"] .sea-banner { display: none; }
/* The canvas fills the strip. This is its CSS size only: the painter sizes the
   backing store in device pixels, so the band stays crisp on a 2x display
   instead of being an upscaled CSS-sized buffer. */
.sea-canvas { display: block; width: 100%; height: 100%; }

/* 12. Month lattice, ported from the admin calendar (crates/app-admin-web/
   styles/app.css), stripped to anonymous per-day occupancy (no per-booking chips):
   a Monday-start grid of day-cell buttons, seven columns and one row per week of
   the month, that helm to the Day view, each showing its date and an aggregate
   occupancy count. The columns may shrink below their words, so a phone's month never
   runs wider than the page (issue #231); there each cell shows the bare count and keeps
   the words for a screen reader. */
.cal-grid.cal-month { grid-template-columns: repeat(7, minmax(0, 1fr)); }
.cal-mhead { padding: var(--space-0-5); text-align: center; background: var(--bg-sunken); color: var(--fg-secondary); }
.cal-mday {
  position: relative; display: flex; flex-direction: column; align-items: stretch; gap: 2px;
  min-height: 88px; padding: var(--space-0-5);
  background: var(--bg-elevated); color: var(--fg-primary);
  border: 1px solid var(--border-default); border-radius: var(--radius-sm);
  cursor: pointer; text-align: left; font: inherit;
}
.cal-mday:hover { background: var(--bg-sunken); }
.cal-mday.other-month { background: var(--bg-base); }
.cal-mday.other-month .cal-mdate { color: var(--fg-muted); }
.cal-mday.today .cal-mdate { color: var(--accent); font-weight: var(--weight-ui); }
.cal-mdate { align-self: flex-end; }
.cal-mcount {
  margin-top: auto; text-align: center; font-size: var(--text-sm);
  padding: 2px 4px; border-radius: var(--radius-sm);
  background: var(--bg-sunken); color: var(--fg-secondary);
}
.cal-mcount.busy { background: var(--accent); color: var(--fg-on-accent); }
.cal-mcount-n { display: none; }
@media (max-width: 640px) {
  .cal-mcount-n { display: inline; }
  .cal-mcount-label {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip-path: inset(50%); white-space: nowrap;
  }
}

/* 20. Hela bladet (docs/superpowers/specs/2026-09-03-hela-bladet-redesign.md):
   the customer surface on the same chart sheet as the admin. The rules mirror
   the admin stylesheet's §20; the customer has no metric cards, and only the
   member's own blocks carry a status stamp. */

/* 20a. The sheet. The night sheet opens with a sky, 360px from a darker ink to the
   base. The depth contours along the top bar, Djupkurvor (DESIGN.md, 2026-10-10),
   replaced the repeating paper that lay here; both web apps draw them from
   ui-tokens/css/chart-corner.css, which reaches this page through theme.css. */
:root[data-theme="dark"] body {
  background-image: linear-gradient(180deg, color-mix(in srgb, var(--bg-base) 70%, black) 0, var(--bg-base) 360px);
  background-repeat: no-repeat;
}

/* 20b. The cartouche. The top bar is the chart's title block: mark, wordmark,
   and an edition line in letter-spaced mono beneath it, on the paper, under the
   bar's one hairline (a second rule below it read as a stray line, dropped
   2026-09-04). The week chip folds into the edition. */
.topbar { position: relative; padding: var(--space-1) var(--space-3); background: transparent; }
.topbar-brand {
  display: grid; grid-template-columns: auto auto; column-gap: var(--space-1);
  align-items: center;
}
.topbar-mark { grid-row: 1 / span 2; }
.topbar-mark svg { width: 34px; height: 34px; }
.topbar-wordmark { font-size: var(--text-xl); line-height: 1.05; }
.topbar-edition {
  grid-column: 2; font-family: var(--font-mono); font-size: 10px; line-height: 1.2;
  letter-spacing: .16em; text-transform: uppercase; color: var(--fg-secondary);
}
.topbar-week { display: none; }
/* Under 360px the 228px edition line is the ten pixels of horizontal overflow
   the phone pass found on 2026-09-04; the mark and the wordmark stay. */
@media (max-width: 360px) { .topbar-edition { display: none; } }

/* The rail band carries a compass rose at its foot: the chart margin's mark. */
.app-shell { position: relative; }
.app-shell::before {
  content: ""; position: absolute; left: 24px; bottom: 32px; width: 160px; height: 160px;
  background: var(--fg-primary); opacity: .05; pointer-events: none;
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'%3E%3Cg fill='none' stroke='%23000' stroke-width='1.5'%3E%3Ccircle cx='100' cy='100' r='96'/%3E%3Ccircle cx='100' cy='100' r='70'/%3E%3Ccircle cx='100' cy='100' r='6'/%3E%3Cpath d='M100 4v192M4 100h192M32 32l136 136M168 32L32 168'/%3E%3C/g%3E%3Cpath fill='%23000' d='M100 12l10 88-10 88-10-88zM12 100l88-10 88 10-88 10z'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'%3E%3Cg fill='none' stroke='%23000' stroke-width='1.5'%3E%3Ccircle cx='100' cy='100' r='96'/%3E%3Ccircle cx='100' cy='100' r='70'/%3E%3Ccircle cx='100' cy='100' r='6'/%3E%3Cpath d='M100 4v192M4 100h192M32 32l136 136M168 32L32 168'/%3E%3C/g%3E%3Cpath fill='%23000' d='M100 12l10 88-10 88-10-88zM12 100l88-10 88 10-88 10z'/%3E%3C/svg%3E");
  -webkit-mask-size: contain; mask-size: contain; -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
}
@media (max-width: 640px) { .app-shell::before { display: none; } }

/* 20c. Headings carry a sounding: 36px Besley with a small wave drawn under the
   first word in the accent ink. */
h1 { font-size: var(--text-3xl); letter-spacing: -.01em; position: relative; padding-bottom: 12px; }
h1::after {
  content: ""; position: absolute; left: 1px; bottom: 0; width: 44px; height: 8px;
  background: var(--accent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 44 8'%3E%3Cpath d='M0 4C5.5 0 11 0 16.5 4S27.5 8 33 4 44 0 44 4' fill='none' stroke='%23000' stroke-width='1.6'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 44 8'%3E%3Cpath d='M0 4C5.5 0 11 0 16.5 4S27.5 8 33 4 44 0 44 4' fill='none' stroke='%23000' stroke-width='1.6'/%3E%3C/svg%3E");
  -webkit-mask-size: contain; mask-size: contain; -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
}

/* 20d. Cards are framed panels: a second rule 4px inside the edge, so the
   neatline ticks sit between two rules the way register marks do. */
.settings-section, .booking-card, .login-form {
  box-shadow: inset 0 0 0 4px var(--bg-elevated), inset 0 0 0 5px var(--border-default), var(--shadow-sm);
}

/* 20g. Login: the compass rose behind the card, so the first screen is the
   same sheet as every other. */
.app-main { position: relative; }
.app-main:has(.login-form)::before {
  content: ""; position: absolute; left: 50%; top: 96px; width: min(340px, 100%); height: 340px;
  transform: translateX(-50%); background: var(--fg-primary); opacity: .06; pointer-events: none;
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'%3E%3Cg fill='none' stroke='%23000' stroke-width='1.5'%3E%3Ccircle cx='100' cy='100' r='96'/%3E%3Ccircle cx='100' cy='100' r='70'/%3E%3Ccircle cx='100' cy='100' r='6'/%3E%3Cpath d='M100 4v192M4 100h192M32 32l136 136M168 32L32 168'/%3E%3C/g%3E%3Cpath fill='%23000' d='M100 12l10 88-10 88-10-88zM12 100l88-10 88 10-88 10z'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'%3E%3Cg fill='none' stroke='%23000' stroke-width='1.5'%3E%3Ccircle cx='100' cy='100' r='96'/%3E%3Ccircle cx='100' cy='100' r='70'/%3E%3Ccircle cx='100' cy='100' r='6'/%3E%3Cpath d='M100 4v192M4 100h192M32 32l136 136M168 32L32 168'/%3E%3C/g%3E%3Cpath fill='%23000' d='M100 12l10 88-10 88-10-88zM12 100l88-10 88 10-88 10z'/%3E%3C/svg%3E");
  -webkit-mask-size: contain; mask-size: contain; -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
}
.login-form { position: relative; }

/* The travelling rail marker has nowhere to be on a route no rail item owns
   (the login page); hide it rather than park it on Logg. */
.app-nav:not(:has(a[aria-current="page"])) .nav-marker { display: none; }

/* 20h. Stamps. Another member's booking is plain grey with its time; the hollow buoy
   it carried went on 2026-10-10, so the calendar has no buoys left. The member's own
   booking carries the status stamp staff see (issue #7): an hourglass while it waits
   for Swish, an exclamation mark once active and still unpaid, a tick once paid. The
   stamp is the ui-tokens SVG in the markup, in the block's ink, placed by the shared
   `.cal-stamp`. In the day's list it sits by the status word, in the status colour. */
.avail-slot.own { padding-left: 22px; }
.cal-entry .cal-stamp { width: 12px; height: 12px; }
.cal-entry.own small { display: flex; align-items: center; gap: 4px; color: var(--status-held); }
.cal-entry.own.paid small { color: var(--status-paid); }
.cal-entry.own.unpaid small { color: var(--status-unpaid); }

/* 20i. Tidvattentabellen: the day view reads as a tide table. Above the row a
   mono line names high water (the busiest hour) and low water (the longest
   empty span). The curve itself is drawn by the shared calendar-rows.css. */
.cal-tide-table {
  display: flex; flex-wrap: wrap; gap: var(--space-0-5) var(--space-3);
  padding: var(--space-0-5) var(--space-1); margin-bottom: var(--space-0-5);
  font-family: var(--font-mono); font-size: var(--text-xs); letter-spacing: .04em;
  color: var(--fg-secondary); border-top: 1px solid var(--border-default); border-bottom: 1px solid var(--border-default);
}
.cal-tide-table strong { color: var(--fg-primary); font-weight: var(--weight-ui); }
.cal-tide-table .tide-mark { display: inline-block; width: 8px; height: 8px; border-radius: 50%; border: 1.5px solid var(--accent); box-sizing: border-box; margin-right: 6px; vertical-align: -1px; }
.cal-tide-table .tide-mark.high { background: var(--accent); }
