/* 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 (hatched water, 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-hairline) solid var(--border-strong); border-radius: var(--radius-sm);
}
.skip-link:focus-visible { top: var(--space-2); }
.mono, .booking-time, .booking-code-value, .avail-slot-time { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.cal-colhead { text-transform: uppercase; letter-spacing: 0.06em; font-size: var(--text-xs); }

/* 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;
}
.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); }
@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. */
button {
  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 { background: var(--accent-hover); }
button:active { transform: scale(0.99); }
button:disabled, input:disabled, select:disabled, textarea:disabled { opacity: .6; cursor: not-allowed; }
.btn-secondary { background: transparent; color: var(--accent); border-color: var(--border-strong); }
.btn-secondary:hover { background: var(--bg-sunken); }
.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); }
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-default); 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); }
.account-feedback-link, .account-logout { margin-right: var(--space-1); }
.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); }
.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); }
/* 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); }
.hold-countdown { margin: var(--space-1) 0 0; color: var(--accent); font-weight: var(--weight-ui); }

/* 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); }
.cal-grid.cal-week { grid-template-columns: auto repeat(7, 1fr); }
/* Borderless, elevated card. The grid draws the separators: `.cal-grid`'s gap
   lets the well's sunken fill show through between columns, so the gap IS the
   rule and no column carries an edge of its own. The radius is whole again now
   that no border has to stop anywhere, and `overflow: clip` keeps the sticky
   header band inside the rounded top. Mirrors the Slint week and day grids. */
.cal-col { background: var(--bg-elevated); border-radius: var(--radius-sm); overflow: clip; }
/* High contrast keeps the column edge. Dropping it in the other themes is a
   deliberate look, but here the gap alone measures 1.15:1 against the card
   where the border measured 19.27:1, and WCAG 1.4.11 asks 3:1 of a boundary
   that carries meaning. This theme exists to make that boundary explicit, so
   it opts back in rather than inheriting the quieter chrome. */
:root[data-theme="high-contrast"] .cal-col { border: 1px solid var(--border-default); }
.cal-colhead {
  position: sticky; top: 0; z-index: 3;
  display: flex; align-items: center; justify-content: center;
  height: var(--cal-header-h); padding: 0 var(--space-0-5);
  background: var(--bg-sunken); color: var(--fg-secondary);
  font-weight: var(--weight-display);
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
}
.cal-colhead.today { color: var(--accent); font-weight: 700; }
/* 24 h window (00:00–24:00) × 40 px/h = 960 px; slot top/height percentages
   resolve against this. Solid 1 px hour hairlines. */
.cal-track {
  position: relative; height: 960px; cursor: pointer;
  background: repeating-linear-gradient(
    to bottom,
    transparent 0, transparent calc(100% / 24 - 1px),
    var(--border-default) calc(100% / 24 - 1px), var(--border-default) calc(100% / 24)
  );
}
.cal-gutter { display: flex; flex-direction: column; width: 28px; }
.cal-gutter-head { visibility: hidden; }
.cal-gutter-track { position: relative; flex: 1; }
.cal-hour {
  position: absolute; right: 2px; transform: translateY(-50%);
  font-family: var(--font-mono); font-size: var(--text-xs);
  font-weight: var(--weight-display);
  color: var(--fg-secondary); line-height: var(--leading-mono);
}
.cal-hour:first-child { transform: none; }

/* 10b. Anonymous hatched water (G-1): an occupied span renders as "sea you
   cannot sail": a hatch fill on theme tokens, its time range as the only text,
   nothing traceable to a booking or its holder. This comment said "an Upptaget
   label" until 2026-08-28; nothing here injects one and the markup has none.
   Free water stays the calm elevated
   surface. The hatch is a repeating-linear-gradient of the muted ink over the
   held tint, status-neutral (it never signals paid/unpaid), non-interactive. */
.avail-slot {
  position: absolute; left: 2px; right: 2px;
  border: 1px solid var(--border-default); border-radius: var(--radius-block);
  padding: 2px 4px; overflow: hidden; line-height: 1.2;
  color: var(--fg-secondary);
  background-color: var(--bg-sunken);
  background-image: repeating-linear-gradient(
    45deg,
    color-mix(in srgb, var(--fg-muted) 22%, transparent) 0,
    color-mix(in srgb, var(--fg-muted) 22%, transparent) 2px,
    transparent 2px, transparent 7px
  );
}
.avail-slot-time { font-size: var(--text-xs); color: var(--fg-secondary); }
/* High contrast: the tint carries no meaning there, so lean on a denser hatch
   and a solid edge for the "occupied" read. */
:root[data-theme="high-contrast"] .avail-slot {
  border-width: 2px; border-color: var(--fg-primary);
  background-image: repeating-linear-gradient(
    45deg, var(--fg-primary) 0, var(--fg-primary) 1px, transparent 1px, transparent 6px
  );
}

/* 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. The lifetime hairline's animation IS the dismissal
   clock; hover pauses both. */
.signalen-stack {
  position: fixed; top: var(--space-2); left: 50%; transform: translateX(-50%);
  display: flex; flex-direction: column; gap: var(--space-1); z-index: 100;
}
.signalen {
  position: relative; overflow: hidden;
  display: flex; align-items: center; gap: var(--space-1);
  min-width: 260px; max-width: 420px;
  padding: var(--space-1) var(--space-2) calc(var(--space-1) + 2px);
  background: var(--bg-inverse); color: var(--fg-inverse);
  border: 1px solid var(--border-strong); border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  animation: signal-in var(--motion-base) var(--ease-standard) backwards;
}
@keyframes signal-in { from { opacity: 0; transform: translateY(12px); } }
.signalen-flag { flex-shrink: 0; }
.signalen-flag svg { width: 14px; height: 14px; display: block; }
.signalen.danger .signalen-flag { color: color-mix(in srgb, var(--danger) 50%, var(--fg-inverse)); }
.signalen.info .signalen-flag { color: color-mix(in srgb, var(--info) 50%, var(--fg-inverse)); }
.signalen.success .signalen-flag { color: color-mix(in srgb, var(--success) 50%, var(--fg-inverse)); }
.signalen.warning .signalen-flag { color: color-mix(in srgb, var(--warning) 50%, var(--fg-inverse)); }
/* FR-28 §7: primary feedback surface at base size, medium weight. */
.signalen-text { flex: 1; font-size: var(--text-base); font-weight: 500; }
.signalen-close {
  background: transparent; border: none; padding: 0 var(--space-0-5);
  color: var(--fg-inverse); opacity: .7; font-size: var(--text-sm);
}
.signalen-close:hover { background: transparent; opacity: 1; }
.signalen-life {
  position: absolute; left: 0; right: 0; bottom: 0; height: 2px;
  background: var(--signal); transform-origin: left;
  animation: signal-life 5s linear forwards;
}
.signalen:hover .signalen-life { animation-play-state: paused; }
@keyframes signal-life { to { transform: scaleX(0); } }
:root[data-theme="high-contrast"] .signalen { border-width: 2px; }
:root[data-theme="high-contrast"] .signalen.danger { border-color: var(--danger); }
:root[data-theme="high-contrast"] .signalen.info { border-color: var(--info); }
:root[data-theme="high-contrast"] .signalen.success { border-color: var(--success); }
:root[data-theme="high-contrast"] .signalen.warning { border-color: var(--warning); }
:root[data-theme="high-contrast"] .signalen-life { background: var(--fg-primary); }

/* 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;
}

/* 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. */
.settings-section, .booking-card, .login-form, .help-hint-text, .signalen, .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) {
  :root { --nav-bar-h: 56px; }
  .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); }
  /* Seven items share 375px on a phone: a label must never widen its item, so
     it ellipsises inside the item's flex share rather than overrunning its
     neighbour ("Feedback" ran into "Inställningar" at 375 on 2026-09-04). Under
     400px the bar is icons only with the active item's label shown, the phone
     bar idiom; every item keeps its `title` and the label stays in the tree
     for screen readers. */
  .app-nav a .nav-label {
    max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  @media (max-width: 400px) {
    .app-nav a:not([aria-current="page"]) .nav-label {
      position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0);
    }
  }
  /* 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)); }
  /* Agenda stack: the week grid linearises to one column and the fixed-height
     hour track collapses so each occupied span reads as a stacked list item. */
  .cal-toolbar { flex-wrap: wrap; gap: var(--space-1); }
  .cal-viewtoggle { margin-left: 0; flex-wrap: wrap; }
  .cal-range { min-width: 0; }
  .cal-grid.cal-week { grid-template-columns: 1fr; }
  .cal-gutter { display: none; }
  .cal-track { height: auto; min-height: 48px; background: none; padding: var(--space-0-5); }
  .avail-slot { position: static; display: block; width: 100% !important; left: auto !important; margin: 2px 0; }
}

/* 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;
}

/* The caption's wave glyph, standing where a dash used to separate the sea-state
   phrase from the booking count. It is generated from the same surface as the
   water below it (`hamnkapten-sea-wave`), carries its own px size in the SVG
   attributes, matching the fixed `--text-*` scale the rest of the type uses, and
   strokes in `currentColor` so it takes the caption's ink in every theme.
   `middle` sits it on the text's centre; the baseline would hang it low. */
.sea-wave-glyph { vertical-align: middle; }
.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 6×7 grid of day-cell buttons that helm to the Day view, each
   showing its date and an aggregate occupancy count. */
.cal-grid.cal-month { grid-template-columns: repeat(7, 1fr); }
.cal-mhead { padding: var(--space-0-5); text-align: center; background: var(--bg-sunken); color: var(--fg-secondary); }
.cal-mday {
  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: 700; }
.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); }

/* 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 no status
   blocks, so the hero and the status buoys are replaced by one buoy on the
   occupied slot. */

/* 20a. Isobath paper. Two fixed layers behind everything: hand-drawn depth
   contours as an SVG mask filled with the ink token, and a 48px dotted
   graticule. Both take their colour from the theme, so the night sheet paints
   its own. High contrast drops both: nothing behind the text is its point. */
body { position: relative; }
body::before, body::after {
  content: ""; position: fixed; inset: 0; z-index: -1; pointer-events: none;
}
body::before {
  background: var(--fg-primary); opacity: .06;
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='480' height='480' viewBox='0 0 480 480'%3E%3Cg fill='none' stroke='%23000' stroke-width='1'%3E%3Cpath d='M-20 120 C 80 60 160 180 260 130 S 400 60 500 120'/%3E%3Cpath d='M-20 240 C 60 200 140 300 250 250 S 400 180 500 240'/%3E%3Cpath d='M-20 380 C 100 320 180 440 300 370 S 430 320 500 380'/%3E%3Cpath d='M300 40 c 40 -30 110 -10 120 30 s -50 70 -100 50 s -60 -50 -20 -80z'/%3E%3Cpath d='M320 55 c 25 -15 70 -5 78 20 s -35 45 -68 32 s -35 -35 -10 -52z'/%3E%3Cpath d='M60 300 c 30 -20 80 -5 85 25 s -40 50 -75 35 s -40 -40 -10 -60z'/%3E%3C/g%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='480' height='480' viewBox='0 0 480 480'%3E%3Cg fill='none' stroke='%23000' stroke-width='1'%3E%3Cpath d='M-20 120 C 80 60 160 180 260 130 S 400 60 500 120'/%3E%3Cpath d='M-20 240 C 60 200 140 300 250 250 S 400 180 500 240'/%3E%3Cpath d='M-20 380 C 100 320 180 440 300 370 S 430 320 500 380'/%3E%3Cpath d='M300 40 c 40 -30 110 -10 120 30 s -50 70 -100 50 s -60 -50 -20 -80z'/%3E%3Cpath d='M320 55 c 25 -15 70 -5 78 20 s -35 45 -68 32 s -35 -35 -10 -52z'/%3E%3Cpath d='M60 300 c 30 -20 80 -5 85 25 s -40 50 -75 35 s -40 -40 -10 -60z'/%3E%3C/g%3E%3C/svg%3E");
  -webkit-mask-size: 480px 480px; mask-size: 480px 480px;
}
body::after {
  background-image: radial-gradient(circle, var(--border-default) .8px, transparent 1px);
  background-size: 48px 48px; opacity: .35;
}
:root[data-theme="dark"] body::before { opacity: .09; }
: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;
}
:root[data-theme="high-contrast"] body::before,
:root[data-theme="high-contrast"] body::after { display: none; }

/* 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 the caption glyph's 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);
}

/* 20f. The calendar graticule: a quarter-hour line every 10px, the hour line
   as before (FR-24). Day headers set in the display voice. */
.cal-track {
  background:
    repeating-linear-gradient(to bottom,
      transparent 0, transparent calc(100% / 96 - 1px),
      color-mix(in srgb, var(--border-default) 45%, transparent) calc(100% / 96 - 1px),
      color-mix(in srgb, var(--border-default) 45%, transparent) calc(100% / 96)),
    repeating-linear-gradient(to bottom,
      transparent 0, transparent calc(100% / 24 - 1px),
      var(--border-default) calc(100% / 24 - 1px), var(--border-default) calc(100% / 24));
}
.cal-colhead { font-family: var(--font-display); text-transform: none; letter-spacing: 0; font-size: var(--text-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. Bojar: an occupied span carries a buoy at its head, a hollow ring
   with a topmark, over the hatched water it already draws. Free water stays
   the calm elevated surface, as before. */
.avail-slot { padding-left: 20px; border-radius: 3px; }
.avail-slot::before {
  content: ""; position: absolute; left: 5px; top: 5px; width: 11px; height: 11px;
  border-radius: 50%; border: 2px solid var(--fg-secondary); box-sizing: border-box;
  background: var(--bg-elevated);
}
.avail-slot::after {
  content: ""; position: absolute; left: 10px; top: 1px; width: 1px; height: 5px;
  background: var(--fg-secondary);
}
.avail-slot { z-index: 1; }

/* 20i. Tidvattentabellen: the day view reads as a tide table. Behind the
   track a curve traces how many saunas are booked hour by hour, drawn like
   the tide curve on a harbour board, and above the track a mono line names
   high water (the busiest hour) and low water (the longest empty span). */
.cal-tide {
  position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; z-index: 0;
}
.cal-tide path.tide-fill { fill: color-mix(in srgb, var(--accent) 10%, transparent); }
.cal-tide path.tide-line { fill: none; stroke: color-mix(in srgb, var(--accent) 55%, transparent); stroke-width: 1.25; vector-effect: non-scaling-stroke; }
.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); }
.cal-tide svg { width: 100%; height: 100%; display: block; }
