/* BoatLink - boat telemetry UI for boatlink.kybetube.com
   Loaded after variables.css / global.css / components.css: re-tints the shared
   design tokens to a marine palette, then adds the boat-specific components. */

/* The browser hides [hidden] elements with a rule any author `display` beats,
   so every flex or grid component here quietly opted out of it - which is how
   the arrange hint stayed on screen outside edit mode however carefully the
   script set the attribute. Hidden means hidden; there is no design in this
   stylesheet that wants an element it has hidden to be visible. */
[hidden] { display: none !important; }

:root {
	/* Marine palette - deep water navy with a channel-marker cyan accent */
	--color-primary: #0ea5e9;
	--color-primary-hover: #0284c7;

	--bg-primary: #071620;
	--bg-secondary: #0d2430;
	--bg-tertiary: #163444;
	--bg-card: #0d2430;

	--text-primary: #eef7fb;
	--text-secondary: #9db6c4;
	--text-muted: #6b8798;

	--border-color: #1c4053;
	--border-focus: #0ea5e9;

	/* Status colours, shared by badges, dots and alert rows */
	--status-online: #10b981;
	--status-late: #f59e0b;
	--status-silent: #ef4444;
	--status-unknown: #6b8798;

	/* The accent at low opacity: the wash behind a hovered icon button, and
	   the ring around a focused input or an in-flight control. These were the
	   accent's rgba written out four times, which meant a second product
	   wearing this component set got cyan hovers over its own palette however
	   carefully it re-tinted the tokens above. */
	--accent-soft: rgba(14, 165, 233, 0.12);
	--accent-ring: rgba(14, 165, 233, 0.25);

	/* The phone shell's two fixed bars. Not --bg-primary: both sit over
	   scrolling content behind a blur, so they are the ground colour at the
	   opacity that lets a little of it through. */
	--shell-appbar: rgba(7, 22, 32, 0.92);
	--shell-tabbar: rgba(13, 36, 48, 0.96);
}

/* ---- Fancy: the boatlink.fi night-watch room, worn by the app ----------
   The classic navy above stays the default. Fancy re-tints the same tokens
   to the demo page's palette - near-black ground, brass accent, warm ink -
   so every component follows without knowing a second theme exists. The
   attribute is set on <html> before stylesheets apply (see index.html) and
   chosen from the sidebar. */
:root[data-bl-theme="fancy"] {
	--color-primary: #c99548;
	--color-primary-hover: #e3b878;

	--bg-primary: #07090b;
	--bg-secondary: #0e1216;
	--bg-tertiary: #141a1f;
	--bg-card: #0e1216;

	--text-primary: #eae5da;
	--text-secondary: #8fa0aa;
	--text-muted: #5b6a73;

	--border-color: #1d252c;
	--border-focus: #c99548;

	--status-online: #46c896;
	--status-late: #e0a33d;
	--status-silent: #ef8078;
	--status-unknown: #5b6a73;
}

body {
	background-image:
		radial-gradient(1100px 500px at 15% -10%, rgba(14, 165, 233, 0.10), transparent 60%),
		radial-gradient(900px 500px at 100% 0%, rgba(16, 185, 129, 0.06), transparent 55%);
	background-attachment: fixed;
}

/* The glow follows the accent: brass over near-black instead of cyan over
   navy. */
:root[data-bl-theme="fancy"] body {
	background-image:
		radial-gradient(1100px 500px at 70% -10%, rgba(201, 149, 72, 0.07), transparent 60%),
		radial-gradient(900px 500px at 0% 0%, rgba(201, 149, 72, 0.04), transparent 55%);
}

/* The theme switch's circular reveal: the default cross-fade would fight
   the clip-path circle the script drives, so both frames hold still and
   the circle alone does the telling. */
::view-transition-old(root),
::view-transition-new(root) {
	animation: none;
	mix-blend-mode: normal;
}

::view-transition-old(root) { z-index: 1; }
::view-transition-new(root) { z-index: 2; }

/* The theme picker in the sidebar: two words, the chosen one lit. */
.bl-theme-pick {
	display: flex;
	gap: 4px;
	margin-bottom: 10px;
	padding: 3px;
	border: 1px solid var(--border-color);
	border-radius: 8px;
	background: var(--bg-primary);
}

.bl-theme-pick button {
	flex: 1;
	padding: 5px 8px;
	border: 0;
	border-radius: 6px;
	background: none;
	color: var(--text-muted);
	font-size: 0.72rem;
	font-weight: 600;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	cursor: pointer;
}

.bl-theme-pick button.active {
	background: var(--bg-tertiary);
	color: var(--text-primary);
}

.sidebar.collapsed .bl-theme-pick { display: none; }

/* ---- the embedded dashboard panel (boat page, Dashboard tab) -----------
   The frame is told its height by the panel inside it, so the page scrolls
   and the panel never does. One look on this side: the panel carries its
   own day/night themes within. */
/* The wrapper carries the height the panel asks for; the frame fills it.
   Sizing the frame itself left its rendered box arguing with this
   stylesheet's opening height, and the leftover painted black between the
   dashboard and the wardrobe below. */
.dash2-wrap {
	position: relative;
	background: #050607;
	border-radius: 16px;
	height: 1100px;
	transition: height 0.2s ease;
	overflow: hidden;
}

.dash2-frame {
	display: block;
	width: 100%;
	height: 100%;
	border: 0;
	border-radius: 16px;
	background: #050607;
}

.dash2-full {
	position: absolute;
	top: 14px;
	right: 18px;
	z-index: 2;
	padding: 8px 14px;
	border-radius: 999px;
	border: 1px solid rgba(255, 255, 255, 0.14);
	background: rgba(8, 11, 16, 0.78);
	color: #c8cdd4;
	font: 700 10px ui-monospace, monospace;
	letter-spacing: 0.12em;
	text-transform: uppercase;
	cursor: pointer;
	backdrop-filter: blur(6px);
	transition: 0.2s ease;
}

.dash2-full:hover { color: #f0d49b; border-color: rgba(201, 149, 72, 0.45); }

/* In full screen the button has said its piece: it fades out a few
   seconds in, so the panel is all there is, and comes back on a touch or
   a moved pointer - it is still the way out. */
.dash2-wrap:fullscreen .dash2-full,
.dash2-wrap:-webkit-full-screen .dash2-full { transition: opacity 0.6s ease; }
.dash2-wrap:fullscreen .dash2-full[data-idle="1"],
.dash2-wrap:-webkit-full-screen .dash2-full[data-idle="1"] { opacity: 0; pointer-events: none; }
/* A screen has no scrollbar to offer: whatever is left over is clipped,
   never scrolled.

   The frame carries an inline height - the panel tells us how tall it is
   and we honour it - and an inline height beats any plain rule, so in
   full screen the frame stayed its content height inside a screen-sized
   box: the spill past the glass, and the black beside it. !important is
   the one thing that outranks an inline style, and it means full screen
   fits WITHOUT depending on a fullscreenchange event that Safari may
   fire late, prefixed, or not at all. */
.dash2-wrap:fullscreen,
.dash2-wrap:-webkit-full-screen { border-radius: 0; overflow: hidden; }
.dash2-wrap:fullscreen .dash2-frame,
.dash2-wrap:-webkit-full-screen .dash2-frame {
	height: 100% !important;
	width: 100% !important;
	border-radius: 0;
	transform: none !important;
}

.bl-dash2-note {
	margin: 10px 2px 0;
	color: var(--text-muted);
	font-size: 0.8rem;
}

@media (max-width: 760px) {
	.dash2-frame { border-radius: 12px; }
	.dash2-full { top: 10px; right: 10px; padding: 7px 11px; }
}

/* ============ SHELL ============ */

.sidebar.collapsed .sidebar-header::before {
	content: '⚓';
}

.bl-logo {
	display: flex;
	align-items: center;
	gap: var(--spacing-sm);
	font-size: var(--font-size-xl);
	font-weight: 700;
	color: var(--text-primary);
	text-decoration: none;
	letter-spacing: -0.02em;
}

.bl-logo:hover {
	color: var(--text-primary);
}

.bl-logo-mark {
	width: 28px;
	height: 28px;
	flex-shrink: 0;
	color: var(--color-primary);
}

.bl-logo-accent {
	color: var(--color-primary);
}

/* Nav badge for the open-alert count */
.nav-item-badge {
	margin-left: auto;
	min-width: 20px;
	padding: 0 6px;
	border-radius: var(--radius-full);
	background: var(--color-danger);
	color: #fff;
	font-size: 0.7rem;
	font-weight: 700;
	line-height: 18px;
	text-align: center;
}

.nav-item-badge:empty,
.nav-item-badge[data-count="0"] {
	display: none;
}

.sidebar.collapsed .nav-item-badge {
	display: none;
}

/* ============ PAGE TRANSITIONS (shared with the kybetube user SPA) ============ */

.content-container {
	transition: opacity 150ms ease;
}

.content-container.fade-out { opacity: 0; }
.content-container.fade-in { opacity: 1; }

.page-loading {
	display: flex;
	align-items: center;
	justify-content: center;
	min-height: 40vh;
}

.spinner {
	width: 32px;
	height: 32px;
	border: 3px solid var(--border-color);
	border-top-color: var(--color-primary);
	border-radius: 50%;
	animation: bl-spin 0.8s linear infinite;
}

@keyframes bl-spin {
	to { transform: rotate(360deg); }
}

/* ============ STATUS ============ */

.bl-status {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	padding: 3px 10px 3px 8px;
	border-radius: var(--radius-full);
	font-size: var(--font-size-sm);
	font-weight: 600;
	white-space: nowrap;
}

.bl-dot {
	width: 8px;
	height: 8px;
	border-radius: 50%;
	flex-shrink: 0;
	background: var(--status-unknown);
}

.bl-status-online { background: rgba(16, 185, 129, 0.12); color: var(--status-online); }
.bl-status-online .bl-dot {
	background: var(--status-online);
	box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.6);
	animation: bl-pulse 2.4s ease-out infinite;
}

.bl-status-late { background: rgba(245, 158, 11, 0.12); color: var(--status-late); }
.bl-status-late .bl-dot { background: var(--status-late); }

.bl-status-silent { background: rgba(239, 68, 68, 0.12); color: var(--status-silent); }
.bl-status-silent .bl-dot { background: var(--status-silent); }

/* The gateway is in contact and cannot reach this device. Read as a problem,
   like silent - but its own colour, because the answer is different: silent
   means go and look, this means it is switched off or something else is holding
   its connection. */
.bl-status-unreachable { background: rgba(245, 158, 11, 0.14); color: var(--status-late); }
.bl-status-unreachable .bl-dot { background: var(--status-late); }

.bl-status-no-data,
.bl-status-no-devices { background: rgba(107, 135, 152, 0.14); color: var(--status-unknown); }

/* Switched off on purpose. Grey rather than amber or red: nothing is wrong,
   and a warning colour on a deliberate choice is what made "not reachable"
   read as a fault in the first place. */
.bl-status-off { background: rgba(107, 135, 152, 0.14); color: var(--status-unknown); }
.bl-status-off .bl-dot { background: var(--status-unknown); }

@keyframes bl-pulse {
	0%   { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.5); }
	70%  { box-shadow: 0 0 0 7px rgba(16, 185, 129, 0); }
	100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
}

@media (prefers-reduced-motion: reduce) {
	.bl-status-online .bl-dot { animation: none; }
	.spinner { animation-duration: 2s; }
	.content-container { transition: none; }
}

/* ============ FLEET ============ */

.bl-fleet-grid {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(330px, 1fr));
	gap: var(--spacing-lg);
}

.bl-boat-card {
	display: block;
	background: var(--bg-card);
	border: 1px solid var(--border-color);
	border-radius: var(--radius-lg);
	padding: var(--spacing-lg);
	color: inherit;
	text-decoration: none;
	position: relative;
	overflow: hidden;
	transition: transform var(--transition-fast), border-color var(--transition-fast), box-shadow var(--transition-fast);
}

.bl-boat-card::before {
	content: '';
	position: absolute;
	inset: 0 auto 0 0;
	width: 3px;
	background: var(--status-unknown);
}

.bl-boat-card[data-status="online"]::before { background: var(--status-online); }
.bl-boat-card[data-status="late"]::before   { background: var(--status-late); }
.bl-boat-card[data-status="silent"]::before { background: var(--status-silent); }

.bl-boat-card:hover {
	transform: translateY(-2px);
	border-color: var(--color-primary);
	box-shadow: 0 12px 28px rgba(0, 0, 0, 0.35);
	color: inherit;
}

.bl-boat-card-head {
	display: flex;
	align-items: flex-start;
	justify-content: space-between;
	gap: var(--spacing-sm);
	margin-bottom: var(--spacing-xs);
}

.bl-boat-name {
	font-size: var(--font-size-lg);
	font-weight: 600;
	color: var(--text-primary);
}

.bl-boat-harbour {
	display: flex;
	align-items: center;
	gap: 5px;
	font-size: var(--font-size-sm);
	color: var(--text-muted);
	margin-bottom: var(--spacing-md);
}

.bl-boat-harbour svg {
	width: 14px;
	height: 14px;
	flex-shrink: 0;
}

/* Headline metric row on a fleet card */
.bl-metrics {
	display: grid;
	grid-template-columns: repeat(3, 1fr);
	gap: var(--spacing-sm);
	padding: var(--spacing-md) 0;
	border-top: 1px solid var(--border-color);
	border-bottom: 1px solid var(--border-color);
	margin-bottom: var(--spacing-md);
}

.bl-metric-label {
	display: block;
	font-size: 0.7rem;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--text-muted);
	margin-bottom: 2px;
}

.bl-metric-value {
	font-size: var(--font-size-xl);
	font-weight: 600;
	color: var(--text-primary);
	font-variant-numeric: tabular-nums;
	line-height: 1.2;
}

.bl-metric-unit {
	font-size: var(--font-size-sm);
	font-weight: 500;
	color: var(--text-secondary);
	margin-left: 1px;
}

.bl-metric-value.is-cold  { color: #60a5fa; }
.bl-metric-value.is-alarm { color: var(--color-danger); }
.bl-metric-value.is-warn  { color: var(--color-warning); }
.bl-metric-value.is-empty { color: var(--text-muted); font-size: var(--font-size-lg); }

.bl-boat-foot {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--spacing-sm);
	font-size: var(--font-size-sm);
	color: var(--text-muted);
}

/* ============ ALERTS ============ */

.bl-alert-list {
	display: flex;
	flex-direction: column;
	gap: var(--spacing-sm);
}

.bl-alert {
	display: flex;
	align-items: flex-start;
	gap: var(--spacing-md);
	padding: var(--spacing-md);
	border-radius: var(--radius-md);
	border: 1px solid var(--border-color);
	background: var(--bg-secondary);
	border-left-width: 3px;
}

.bl-alert-critical { border-left-color: var(--color-danger); }
.bl-alert-warning  { border-left-color: var(--color-warning); }
.bl-alert-info     { border-left-color: var(--text-muted); }

.bl-alert-icon {
	width: 20px;
	height: 20px;
	flex-shrink: 0;
	margin-top: 2px;
}

.bl-alert-critical .bl-alert-icon { color: var(--color-danger); }
.bl-alert-warning .bl-alert-icon  { color: var(--color-warning); }
.bl-alert-info .bl-alert-icon     { color: var(--text-muted); }

.bl-alert-body { flex: 1; min-width: 0; }

.bl-alert-title {
	font-weight: 600;
	color: var(--text-primary);
	margin-bottom: 2px;
}

.bl-alert-detail {
	font-size: var(--font-size-sm);
	color: var(--text-secondary);
	overflow-wrap: anywhere;
}

.bl-alert-boat {
	font-size: var(--font-size-sm);
	color: var(--color-primary);
	white-space: nowrap;
	text-decoration: none;
}

/* Compact banner used at the top of a boat page */
.bl-alert-strip {
	display: flex;
	flex-wrap: wrap;
	gap: var(--spacing-sm);
	margin-bottom: var(--spacing-lg);
}

/* ============ BOAT DETAIL ============ */

.bl-boat-header {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--spacing-md);
	margin-bottom: var(--spacing-xl);
}

.bl-back {
	display: inline-flex;
	align-items: center;
	gap: 4px;
	font-size: var(--font-size-sm);
	color: var(--text-secondary);
	text-decoration: none;
	margin-bottom: var(--spacing-sm);
}

.bl-back:hover { color: var(--color-primary); }

.bl-back svg { width: 16px; height: 16px; }

.bl-compartment {
	margin-bottom: var(--spacing-xl);
}

.bl-compartment-head {
	display: flex;
	align-items: baseline;
	gap: var(--spacing-sm);
	margin-bottom: var(--spacing-md);
	padding-bottom: var(--spacing-sm);
	border-bottom: 1px solid var(--border-color);
}

.bl-compartment-name {
	font-size: var(--font-size-lg);
	font-weight: 600;
	color: var(--text-primary);
}

.bl-compartment-meta {
	font-size: var(--font-size-sm);
	color: var(--text-muted);
}

.bl-sensor-grid {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
	gap: var(--spacing-md);
}

.bl-sensor-card {
	background: var(--bg-card);
	border: 1px solid var(--border-color);
	border-radius: var(--radius-lg);
	padding: var(--spacing-md);
}

.bl-sensor-head {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--spacing-sm);
	margin-bottom: var(--spacing-md);
}

.bl-sensor-name {
	font-weight: 600;
	color: var(--text-primary);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.bl-reading-row {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	gap: var(--spacing-sm);
	padding: 6px 0;
}

.bl-reading-row + .bl-reading-row {
	border-top: 1px solid rgba(255, 255, 255, 0.04);
}

.bl-reading-name {
	display: flex;
	align-items: center;
	gap: 6px;
	font-size: var(--font-size-sm);
	color: var(--text-secondary);
}

.bl-reading-name svg {
	width: 14px;
	height: 14px;
	opacity: 0.7;
	flex-shrink: 0;
}

.bl-reading-value {
	font-weight: 600;
	color: var(--text-primary);
	font-variant-numeric: tabular-nums;
}

/* A latching alarm reads as a word, coloured by which word it is: "Water"
   should catch the eye across a cabin, "Dry" should reassure without shouting. */
.bl-value-alarm {
	color: var(--danger, #f87171);
}

.bl-value-ok {
	color: var(--success, #34d399);
}

/* The history line under a bilge reading: when it last had water, how long. */
.bl-reading-note {
	padding: 0 0 6px;
	font-size: var(--font-size-sm);
	color: var(--text-muted);
}

.bl-note-alarm {
	color: var(--danger, #f87171);
}

.bl-sensor-foot {
	margin-top: var(--spacing-sm);
	padding-top: var(--spacing-sm);
	border-top: 1px solid var(--border-color);
	font-size: var(--font-size-sm);
	color: var(--text-muted);
}

/* Battery strength bar */
.bl-batt {
	display: inline-flex;
	align-items: center;
	gap: 6px;
}

.bl-batt-track {
	width: 34px;
	height: 6px;
	border-radius: var(--radius-full);
	background: var(--bg-tertiary);
	overflow: hidden;
}

.bl-batt-fill {
	height: 100%;
	border-radius: var(--radius-full);
	background: var(--status-online);
	transition: width var(--transition-base);
}

.bl-batt-fill.is-low  { background: var(--color-danger); }
.bl-batt-fill.is-warn { background: var(--color-warning); }

/* ============ CHART ============ */

.bl-chart-toolbar {
	display: flex;
	flex-wrap: wrap;
	gap: var(--spacing-sm);
	align-items: center;
}

.bl-range-group {
	display: inline-flex;
	background: var(--bg-tertiary);
	border-radius: var(--radius-md);
	padding: 2px;
	gap: 2px;
}

.bl-range-btn {
	padding: 4px 12px;
	border: none;
	background: transparent;
	color: var(--text-secondary);
	border-radius: var(--radius-sm);
	font-size: var(--font-size-sm);
	font-weight: 500;
	transition: all var(--transition-fast);
}

.bl-range-btn:hover { color: var(--text-primary); }

.bl-range-btn.active {
	background: var(--color-primary);
	color: #fff;
}

.form-select,
.form-select-sm {
	padding: var(--spacing-xs) var(--spacing-sm);
	background-color: var(--bg-tertiary);
	border: 1px solid var(--border-color);
	border-radius: var(--radius-md);
	color: var(--text-primary);
}

.form-select:focus {
	outline: none;
	border-color: var(--border-focus);
}

.bl-chart-container {
	position: relative;
	height: 340px;
}

/* Window paging under the chart */
.bl-chart-nav {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--spacing-sm);
	margin: var(--spacing-sm) 0 var(--spacing-xs);
}

.bl-nav-btn {
	display: inline-flex;
	align-items: center;
	gap: 4px;
	padding: 6px 10px;
	border: 1px solid var(--border-color);
	border-radius: var(--radius-md);
	background: var(--bg-tertiary);
	color: var(--text-secondary);
	font-size: var(--font-size-sm);
	font-weight: 500;
	transition: color var(--transition-fast), border-color var(--transition-fast);
}

.bl-nav-btn svg {
	width: 16px;
	height: 16px;
}

.bl-nav-btn:hover:not(:disabled) {
	color: var(--text-primary);
	border-color: var(--color-primary);
}

.bl-nav-btn:disabled {
	opacity: 0.4;
	cursor: not-allowed;
}

.bl-chart-window {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 2px;
	min-width: 0;
	font-size: var(--font-size-sm);
	color: var(--text-muted);
	text-align: center;
	font-variant-numeric: tabular-nums;
}

.bl-now-btn {
	padding: 0;
	border: none;
	background: none;
	color: var(--color-primary);
	font-size: var(--font-size-sm);
	font-weight: 600;
}

.bl-now-btn[hidden] { display: none; }

/* ============ EMPTY STATES ============ */

.empty-state {
	text-align: center;
	padding: var(--spacing-2xl) var(--spacing-lg);
	color: var(--text-secondary);
}

.empty-state svg {
	width: 48px;
	height: 48px;
	color: var(--text-muted);
	opacity: 0.5;
	margin-bottom: var(--spacing-md);
}

.empty-state h3 {
	font-size: var(--font-size-lg);
	color: var(--text-primary);
	margin-bottom: var(--spacing-xs);
	font-weight: 600;
}

.empty-state p {
	max-width: 46ch;
	margin: 0 auto;
	font-size: var(--font-size-sm);
}

/* ============ SETTINGS ============ */

.bl-settings-grid {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
	gap: var(--spacing-lg);
	align-items: start;
}

/* ============ PHONE SHELL ============

   On phones the fixed sidebar is replaced by an app bar and a bottom tab bar,
   so nav sits under the thumb and the full screen width goes to the data.
   Both are inert above 768px; the sidebar is the navigation there. */

.bl-appbar,
.bl-tabbar,
.bl-only-mobile {
	display: none;
}

.bl-appbar-back,
.bl-appbar-brand,
.bl-appbar-title,
.bl-appbar-user {
	display: none;
}

/* ============ RESPONSIVE ============ */

@media (max-width: 768px) {
	.sidebar { display: none; }

	/* App bar: brand on the top-level pages, back + boat name on a boat page. */
	.bl-appbar {
		position: fixed;
		top: 0;
		left: 0;
		right: 0;
		z-index: 90;
		display: flex;
		align-items: center;
		gap: var(--spacing-sm);
		height: calc(56px + env(safe-area-inset-top));
		padding: env(safe-area-inset-top) var(--spacing-md) 0;
		background: var(--shell-appbar);
		backdrop-filter: blur(10px);
		-webkit-backdrop-filter: blur(10px);
		border-bottom: 1px solid var(--border-color);
	}

	.bl-appbar-brand {
		display: flex;
		align-items: center;
		gap: var(--spacing-sm);
		flex: 1;
		min-width: 0;
		font-size: var(--font-size-lg);
		font-weight: 700;
		color: var(--text-primary);
		text-decoration: none;
		letter-spacing: -0.02em;
	}

	.bl-appbar-brand .bl-logo-mark {
		width: 24px;
		height: 24px;
	}

	.bl-appbar.is-detail .bl-appbar-brand { display: none; }

	.bl-appbar.is-detail .bl-appbar-back,
	.bl-appbar.is-detail .bl-appbar-title {
		display: flex;
	}

	.bl-appbar-back {
		align-items: center;
		justify-content: center;
		width: 40px;
		height: 40px;
		margin-left: calc(var(--spacing-sm) * -1);
		flex-shrink: 0;
		background: none;
		border: none;
		border-radius: var(--radius-full);
		color: var(--text-primary);
		-webkit-tap-highlight-color: transparent;
	}

	.bl-appbar-back:active { background: rgba(255, 255, 255, 0.08); }

	.bl-appbar-back svg {
		width: 24px;
		height: 24px;
	}

	.bl-appbar-title {
		flex: 1;
		min-width: 0;
		align-items: center;
		font-size: var(--font-size-lg);
		font-weight: 600;
		color: var(--text-primary);
		white-space: nowrap;
		overflow: hidden;
		text-overflow: ellipsis;
	}

	.bl-appbar-user {
		display: flex;
		align-items: center;
		justify-content: center;
		width: 34px;
		height: 34px;
		flex-shrink: 0;
		border-radius: var(--radius-full);
		background: var(--color-primary);
		color: #fff;
		font-weight: 600;
		text-decoration: none;
		-webkit-tap-highlight-color: transparent;
	}

	/* Tab bar */
	.bl-tabbar {
		position: fixed;
		bottom: 0;
		left: 0;
		right: 0;
		z-index: 90;
		display: grid;
		grid-template-columns: repeat(3, 1fr);
		background: var(--shell-tabbar);
		backdrop-filter: blur(12px);
		-webkit-backdrop-filter: blur(12px);
		border-top: 1px solid var(--border-color);
		padding-bottom: env(safe-area-inset-bottom);
	}

	.bl-tab {
		position: relative;
		display: flex;
		flex-direction: column;
		align-items: center;
		justify-content: center;
		gap: 3px;
		min-height: 58px;
		padding: var(--spacing-sm) var(--spacing-xs);
		color: var(--text-muted);
		text-decoration: none;
		font-size: 0.7rem;
		font-weight: 600;
		letter-spacing: 0.01em;
		-webkit-tap-highlight-color: transparent;
		transition: color var(--transition-fast);
	}

	.bl-tab svg {
		width: 24px;
		height: 24px;
	}

	.bl-tab.active { color: var(--color-primary); }
	.bl-tab:active { background: rgba(255, 255, 255, 0.05); }

	.bl-tab-badge {
		position: absolute;
		top: 6px;
		left: 50%;
		margin-left: 4px;
		min-width: 18px;
		padding: 0 5px;
		border-radius: var(--radius-full);
		background: var(--color-danger);
		color: #fff;
		font-size: 0.65rem;
		font-weight: 700;
		line-height: 18px;
		text-align: center;
	}

	.bl-tab-badge:empty,
	.bl-tab-badge[data-count="0"] {
		display: none;
	}

	/* Content sits between the two bars, clear of the notch and home indicator. */
	.main-content,
	body.sidebar-collapsed .main-content {
		margin-left: 0;
		padding:
			calc(56px + env(safe-area-inset-top) + var(--spacing-md))
			var(--spacing-md)
			calc(58px + env(safe-area-inset-bottom) + var(--spacing-lg));
	}

	.bl-only-mobile { display: block; }

	/* The app bar owns the back affordance on a boat page. */
	.bl-back { display: none; }

	/* ---- Density ---- */

	.page-header { margin-bottom: var(--spacing-lg); }
	.page-title { font-size: var(--font-size-xl); }
	.page-subtitle { font-size: var(--font-size-sm); }

	.stats-grid {
		grid-template-columns: repeat(2, 1fr);
		gap: var(--spacing-sm);
		margin-bottom: var(--spacing-lg);
	}

	.stat-card { padding: var(--spacing-md); }
	.stat-label { font-size: 0.75rem; }
	.stat-value { font-size: var(--font-size-xl); }

	.bl-fleet-grid,
	.bl-sensor-grid,
	.bl-settings-grid {
		grid-template-columns: 1fr;
		gap: var(--spacing-md);
	}

	.bl-boat-card { padding: var(--spacing-md) var(--spacing-md) var(--spacing-md) var(--spacing-lg); }
	.bl-metrics { padding: var(--spacing-sm) 0; }

	.bl-boat-header { margin-bottom: var(--spacing-lg); }
	.bl-compartment { margin-bottom: var(--spacing-lg); }
	.bl-compartment-head { flex-wrap: wrap; }

	.bl-alert { padding: var(--spacing-sm) var(--spacing-md); }

	/* ---- Chart ---- */

	.card-header {
		flex-direction: column;
		align-items: flex-start;
		gap: var(--spacing-sm);
	}

	.bl-chart-toolbar { width: 100%; }

	.bl-chart-toolbar .form-select-sm {
		flex: 1 1 100%;
		width: 100%;
	}

	.bl-range-group {
		display: flex;
		width: 100%;
	}

	.bl-range-btn {
		flex: 1;
		padding: 10px 0;
		font-size: var(--font-size-base);
	}

	.bl-chart-container { height: 240px; }

	.bl-nav-btn {
		min-height: 40px;
		padding: 6px 12px;
	}

	/* Labels would crowd the window text out on a narrow screen; the arrows
	   carry the meaning on their own. */
	.bl-nav-btn span { display: none; }

	/* ---- Touch targets ---- */

	/* 16px inputs: anything smaller makes iOS Safari zoom the page on focus. */
	.form-input,
	.form-select,
	.form-select-sm {
		font-size: 16px;
		padding: var(--spacing-sm) var(--spacing-md);
	}

	.btn {
		min-height: 44px;
		padding: var(--spacing-sm) var(--spacing-lg);
	}

	.bl-settings-grid .btn,
	.bl-only-mobile .btn {
		width: 100%;
	}

	/* ---- Toasts clear the tab bar ---- */

	.toast-container {
		top: auto;
		left: var(--spacing-md);
		right: var(--spacing-md);
		bottom: calc(58px + env(safe-area-inset-bottom) + var(--spacing-sm));
	}

	.toast {
		min-width: 0;
		max-width: none;
	}

	/* ---- Login ---- */

	.login-card { padding: var(--spacing-xl) var(--spacing-lg); }
	.login-btn { min-height: 48px; }
}

/* Hover lift is a mouse affordance; on touch it sticks after a tap. */
@media (hover: none) {
	.bl-boat-card:hover {
		transform: none;
		box-shadow: none;
		border-color: var(--border-color);
	}

	.bl-boat-card:active {
		transform: scale(0.985);
		border-color: var(--color-primary);
	}
}

/* Small phones (iPhone SE and similar) */
@media (max-width: 380px) {
	.bl-metric-value { font-size: var(--font-size-lg); }
	.bl-metric-label { font-size: 0.65rem; }
	.stat-value { font-size: var(--font-size-lg); }
}

/* Landscape: give the chart what little height there is */
@media (max-height: 480px) and (orientation: landscape) {
	.bl-chart-container { height: 200px; }
}

/* ---- Switch panel control ------------------------------------------- */

.bl-switch-card { margin-bottom: var(--spacing-xl); }

.bl-switch-status {
	font-size: var(--font-size-sm);
	color: var(--text-muted);
	display: inline-flex;
	align-items: center;
	gap: 6px;
}

/* A live socket is worth showing, because the alternative - a queued change
   that lands half an hour later - looks identical otherwise. */
.bl-live-dot {
	width: 8px; height: 8px; border-radius: 50%;
	background: var(--success, #34d399);
	box-shadow: 0 0 0 0 rgba(52, 211, 153, 0.6);
	animation: bl-live-pulse 2s infinite;
}

@keyframes bl-live-pulse {
	70%  { box-shadow: 0 0 0 8px rgba(52, 211, 153, 0); }
	100% { box-shadow: 0 0 0 0 rgba(52, 211, 153, 0); }
}

.bl-switch-grid {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
	gap: 4px var(--spacing-lg);
}

.bl-switch-row {
	display: flex;
	align-items: center;
	gap: var(--spacing-sm);
	padding: 8px 0;
	border-bottom: 1px solid rgba(255, 255, 255, 0.04);
}

.bl-switch-name { flex: 1; font-size: var(--font-size-sm); color: var(--text-secondary); }

.bl-switch-state {
	min-width: 4.5em;
	text-align: right;
	font-size: var(--font-size-sm);
	color: var(--text-muted);
	font-variant-numeric: tabular-nums;
}

.bl-switch {
	position: relative;
	width: 44px; height: 24px;
	border-radius: 12px;
	border: 1px solid var(--border-color);
	background: rgba(255, 255, 255, 0.08);
	cursor: pointer;
	padding: 0;
	transition: background 0.15s ease;
	flex-shrink: 0;
}

.bl-switch-knob {
	position: absolute;
	top: 2px; left: 2px;
	width: 18px; height: 18px;
	border-radius: 50%;
	background: var(--text-secondary);
	transition: transform 0.15s ease, background 0.15s ease;
}

.bl-switch.on { background: var(--success, #34d399); }
.bl-switch.on .bl-switch-knob { transform: translateX(20px); background: #fff; }

/* Pending is deliberately distinct from both on and off: the switch has been
   asked to move and has not yet confirmed. */
.bl-switch.pending { opacity: 0.6; }
.bl-switch.pending .bl-switch-knob { animation: bl-blink 0.8s infinite; }

@keyframes bl-blink { 50% { opacity: 0.35; } }

.bl-switch:disabled { opacity: 0.35; cursor: not-allowed; }

/* A reading that just arrived over the socket. Brief, so a value changing
   while someone is looking at it does not change silently. */
.bl-value-fresh { animation: bl-fresh 1.2s ease-out; }

@keyframes bl-fresh {
	0%   { color: var(--color-primary); }
	100% { color: inherit; }
}

@media (prefers-reduced-motion: reduce) {
	.bl-value-fresh { animation: none; }
}

/* Why a switch did not move. Usually the owner's own phone app holding the
   panel, which allows one connection at a time. */
.bl-switch-error {
	margin: 0 0 var(--spacing-sm);
	padding: 8px 10px;
	border-radius: var(--radius-md);
	background: rgba(239, 68, 68, 0.12);
	color: var(--status-silent);
	font-size: var(--font-size-sm);
}

.bl-switch-card .bl-sensor-foot {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--spacing-sm);
}

.bl-switch-refresh {
	background: none;
	border: 1px solid var(--border-color);
	border-radius: var(--radius-md);
	padding: 4px 10px;
	color: var(--text-secondary);
	font-size: var(--font-size-sm);
	cursor: pointer;
}

.bl-switch-refresh:hover:not(:disabled) {
	border-color: var(--color-primary);
	color: var(--text-primary);
}

.bl-switch-refresh:disabled { opacity: 0.4; cursor: not-allowed; }

/* Undo a switch the boat has not collected yet. Only offered while the
   command is still queued - once it has been handed over it may already
   have happened. */
.bl-switch-cancel {
	margin-left: 8px;
	background: none;
	border: none;
	padding: 0;
	color: var(--color-primary);
	font-size: var(--font-size-sm);
	text-decoration: underline;
	cursor: pointer;
}

.bl-switch-cancel:hover { color: var(--text-primary); }

/* Gateway polling mode. A fast poll costs battery, so the choice is the
   owner's rather than something the system decides silently. */
.bl-mode-card { margin-bottom: var(--spacing-lg); }

.bl-mode-row {
	display: flex;
	align-items: center;
	gap: var(--spacing-md);
	flex-wrap: wrap;
}

.bl-mode-group {
	display: inline-flex;
	border: 1px solid var(--border-color);
	border-radius: var(--radius-md);
	overflow: hidden;
}

.bl-mode-btn {
	background: none;
	border: none;
	padding: 6px 14px;
	color: var(--text-secondary);
	font-size: var(--font-size-sm);
	cursor: pointer;
}

.bl-mode-btn + .bl-mode-btn { border-left: 1px solid var(--border-color); }
.bl-mode-btn:hover:not(.active) { color: var(--text-primary); }

.bl-mode-btn.active {
	background: var(--color-primary);
	color: #fff;
	font-weight: 600;
}

.bl-mode-hint {
	font-size: var(--font-size-sm);
	color: var(--text-muted);
}

/* Time since the gateway last checked in, counting up between polls. Makes
   the cadence visible instead of something you infer from things changing. */
.bl-poll-age {
	margin-left: auto;
	font-size: var(--font-size-sm);
	color: var(--text-muted);
	font-variant-numeric: tabular-nums;
	white-space: nowrap;
}

/* Lost the server. Everything in the card is now history, so it should not
   look like live state. */
.bl-stale { opacity: 0.55; }

/* ---- Diesel-heater control card ---------------------------------------- */
.bl-switch-status.on { color: var(--status-online); }
.bl-heater-controls {
	display: flex;
	flex-wrap: wrap;
	gap: var(--spacing-sm);
	align-items: center;
	margin: var(--spacing-sm) 0;
}
.bl-heater-power,
.bl-heater-vent,
.bl-heater-pump {
	padding: 8px 14px;
	border-radius: var(--radius-md, 8px);
	border: 1px solid var(--border-color);
	background: var(--bg-tertiary);
	color: var(--text-primary);
	cursor: pointer;
	font-weight: 600;
}
.bl-heater-power.on { background: var(--status-online); border-color: var(--status-online); color: #04120b; }
.bl-heater-vent.on { background: var(--color-primary); border-color: var(--color-primary); color: #04121a; }
.bl-heater-power:disabled,
.bl-heater-vent:disabled,
.bl-heater-pump:disabled,
.bl-heater-slider:disabled { opacity: 0.45; cursor: not-allowed; }
/* Setpoint sliders: each takes a full row under the toggle buttons. */
.bl-heater-slider-row {
	flex: 1 1 100%;
	display: flex;
	flex-direction: column;
	gap: 4px;
}
.bl-heater-slider-row label {
	display: flex;
	align-items: center;
	gap: var(--spacing-sm);
	color: var(--text-secondary);
	font-size: var(--font-size-sm, 0.85rem);
}
.bl-heater-slider-val {
	color: var(--text-primary);
	font-weight: 600;
	font-variant-numeric: tabular-nums;
	min-width: 3ch;
}
.bl-heater-slider-row.pending .bl-heater-slider-val { color: var(--color-primary); }
.bl-heater-setting {
	display: inline-flex;
	align-items: center;
	color: var(--color-primary);
	font-size: var(--font-size-xs, 0.75rem);
}
.bl-heater-slider {
	width: 100%;
	height: 24px;
	cursor: pointer;
	accent-color: var(--color-primary);
}
.bl-heater-metrics {
	display: grid;
	grid-template-columns: repeat(3, 1fr);
	gap: var(--spacing-sm);
	margin-top: var(--spacing-sm);
}
.bl-heater-metrics div {
	display: flex;
	flex-direction: column;
	padding: 6px 8px;
	background: var(--bg-tertiary);
	border-radius: var(--radius-md, 8px);
}
.bl-heater-metrics span { color: var(--text-muted); font-size: var(--font-size-xs, 0.75rem); }
.bl-heater-metrics b { color: var(--text-primary); }

/* ---- Heater control feedback (in-flight) ------------------------------- */
/* Reuses the existing bl-spin keyframe (defined near the top). */
.bl-spin {
	display: inline-block;
	width: 12px; height: 12px;
	margin-left: 6px;
	vertical-align: -1px;
	border: 2px solid currentColor;
	border-right-color: transparent;
	border-radius: 50%;
	animation: bl-spin 0.7s linear infinite;
}
/* Two classes beats :disabled at equal specificity via source order, so the
   in-flight look wins over the dimmed disabled state. */
.bl-heater-power.pending,
.bl-heater-vent.pending,
.bl-heater-pump.pending {
	opacity: 1;
	border-color: var(--color-primary);
	box-shadow: 0 0 0 2px var(--accent-ring);
	cursor: progress;
}
.bl-heater-slider-row.pending .bl-heater-slider-val {
	animation: bl-heater-pulse 1s ease-in-out infinite;
}
/* Own name, not the shared bl-pulse (which drives the status dot's box-shadow). */
@keyframes bl-heater-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }

@media (prefers-reduced-motion: reduce) {
	.bl-spin { animation-duration: 2s; }
	.bl-heater-slider-row.pending .bl-heater-slider-val { animation: none; }
}

/* Last-known values being re-confirmed after a page load: the read-outs dim and
   the header shows "refreshing…" until the device's own report lands. Controls
   (sliders, toggles, buttons) stay full-strength - they work off last-known. */
.bl-refreshing .bl-heater-metrics,
.bl-refreshing .bl-heater-slider-val,
.bl-refreshing .bl-switch-state { opacity: 0.5; }
.bl-refreshing .bl-switch-status { color: var(--text-muted); }

/* Passive (power-save) read-only view: setpoints shown, no controls. */
.bl-heater-setpoints {
	display: flex;
	flex-wrap: wrap;
	gap: var(--spacing-md);
	margin: var(--spacing-sm) 0;
}
.bl-heater-setpoints div { display: flex; flex-direction: column; }
.bl-heater-setpoints span { color: var(--text-muted); font-size: var(--font-size-xs, 0.75rem); }
.bl-heater-setpoints b { color: var(--text-primary); font-size: var(--font-size-lg, 1.05rem); }
.bl-heater-savenote {
	margin-top: var(--spacing-sm);
	padding: 8px 10px;
	border-radius: var(--radius-md, 8px);
	background: rgba(245, 158, 11, 0.10);
	color: var(--status-late);
	font-size: var(--font-size-sm, 0.85rem);
}

/* Remote gateway restart control */
.bl-mode-foot {
	display: flex;
	align-items: center;
	gap: var(--spacing-sm);
	margin-top: var(--spacing-sm);
	padding-top: var(--spacing-sm);
	border-top: 1px solid var(--border-color);
}
.bl-gw-reboot {
	padding: 6px 12px;
	border-radius: var(--radius-md, 8px);
	border: 1px solid var(--border-color);
	background: var(--bg-tertiary);
	color: var(--text-secondary);
	cursor: pointer;
	font-weight: 600;
}
.bl-gw-reboot:hover:not(:disabled) { border-color: var(--status-late); color: var(--text-primary); }
.bl-gw-reboot:disabled { opacity: 0.45; cursor: not-allowed; }
.bl-gw-reboot.pending { color: var(--status-late); cursor: progress; opacity: 1; }

/* ---- Unreachable device / gateway-offline states ---------------------- */
/* Device the gateway can't reach right now: read-outs greyed, controls locked. */
.bl-unreach-note {
	margin-top: var(--spacing-sm);
	padding: 8px 10px;
	border-radius: var(--radius-md, 8px);
	background: rgba(239, 68, 68, 0.10);
	color: var(--status-silent);
	font-size: var(--font-size-sm, 0.85rem);
	cursor: help;
}
.bl-boat-banner {
	margin-bottom: var(--spacing-md);
	padding: 10px 14px;
	border-radius: var(--radius-md, 8px);
	background: rgba(239, 68, 68, 0.12);
	border: 1px solid rgba(239, 68, 68, 0.30);
	color: var(--status-silent);
	font-weight: 600;
}

/* This page losing its socket is not the boat losing contact. Said quietly,
   because it is our problem and it fixes itself. */
.bl-boat-banner-soft {
	background: rgba(148, 163, 184, 0.12);
	border-color: rgba(148, 163, 184, 0.28);
	color: var(--text-secondary);
	font-weight: 400;
}

#bl-reboot-timer { font-variant-numeric: tabular-nums; font-weight: 600; color: var(--status-late); }

/* ── Renaming ────────────────────────────────────────────────────────────────
   A name the user can change in place. The pencil is invisible until the row
   is hovered (or the control focused) so a boat page full of sensors does not
   read as a page full of buttons; on touch, where there is no hover, it stays
   faintly visible instead of being unreachable. */
/* The name itself, inside a renameable thing. Truncated rather than wrapped:
   a long name must not push the pencil off the end of the row. */
.bl-name-text {
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.bl-nameable {
	display: inline-flex;
	align-items: center;
	gap: var(--spacing-xs, 4px);
	min-width: 0;
	max-width: 100%;
}

.bl-rename-btn {
	flex: none;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 22px;
	height: 22px;
	padding: 0;
	border: 0;
	border-radius: var(--radius-sm, 4px);
	background: transparent;
	color: var(--text-muted);
	cursor: pointer;
	opacity: 0;
	transition: opacity 0.12s ease, color 0.12s ease, background 0.12s ease;
}

.bl-rename-btn svg {
	width: 13px;
	height: 13px;
}

.bl-nameable:hover .bl-rename-btn,
.bl-rename-btn:focus-visible {
	opacity: 1;
}

.bl-rename-btn:hover {
	color: var(--color-primary);
	background: var(--accent-soft);
}

/* No hover to reveal it, so it is always there - just quiet. */
@media (hover: none) {
	.bl-rename-btn { opacity: 0.45; }
}

.bl-rename-input {
	min-width: 6em;
	max-width: 100%;
	padding: 2px 6px;
	border: 1px solid var(--border-focus);
	border-radius: var(--radius-sm, 4px);
	background: var(--bg-primary);
	/* Inherit so the box matches the heading it replaces rather than snapping
	   the layout to a form-control size mid-edit. */
	color: var(--text-primary);
	font: inherit;
	line-height: inherit;
}

.bl-rename-input:focus {
	outline: none;
	box-shadow: 0 0 0 2px var(--accent-ring);
}

/* The switch rows are a tight grid; keep the name column from growing when
   its pencil appears. */
.bl-switch-name .bl-nameable { max-width: 100%; }

/* ── Compartment management ──────────────────────────────────────────────── */
.bl-compartment-add {
	display: flex;
	justify-content: center;
	margin: var(--spacing-lg, 20px) 0 var(--spacing-xl, 28px);
}

.bl-add-compartment {
	display: inline-flex;
	align-items: center;
	gap: var(--spacing-xs, 4px);
	padding: 8px 16px;
	border: 1px dashed var(--border-color);
	border-radius: var(--radius-md, 8px);
	background: transparent;
	color: var(--text-secondary);
	font-size: var(--font-size-sm, 0.85rem);
	cursor: pointer;
	transition: border-color 0.12s ease, color 0.12s ease;
}

.bl-add-compartment:hover {
	border-color: var(--color-primary);
	color: var(--color-primary);
}

.bl-add-compartment svg { width: 15px; height: 15px; }

/* Same quiet-until-hovered treatment as the rename pencil, so a boat page is
   not a wall of destructive buttons. */
.bl-compartment-remove {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 24px;
	height: 24px;
	margin-left: var(--spacing-xs, 4px);
	padding: 0;
	border: 0;
	border-radius: var(--radius-sm, 4px);
	background: transparent;
	color: var(--text-muted);
	cursor: pointer;
	opacity: 0;
	transition: opacity 0.12s ease, color 0.12s ease, background 0.12s ease;
}

.bl-compartment-remove svg { width: 14px; height: 14px; }

.bl-compartment-head:hover .bl-compartment-remove,
.bl-compartment-remove:focus-visible { opacity: 1; }

.bl-compartment-remove:hover {
	color: var(--status-silent);
	background: rgba(239, 68, 68, 0.12);
}

@media (hover: none) {
	.bl-compartment-remove { opacity: 0.45; }
}

.bl-compartment-empty {
	color: var(--text-muted);
	font-size: var(--font-size-sm, 0.85rem);
	padding: var(--spacing-md, 12px) 0;
}

/* Sits in the sensor card footer, which is a flex row. */
.bl-move-device {
	flex: none;
	max-width: 11em;
	padding: 2px 6px;
	border: 1px solid var(--border-color);
	border-radius: var(--radius-sm, 4px);
	background: var(--bg-primary);
	color: var(--text-secondary);
	font-size: var(--font-size-sm, 0.8rem);
}

.bl-sensor-foot {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--spacing-sm, 8px);
}

.bl-new-compartment {
	min-width: 14em;
	text-align: center;
}

/* ── Adding a boat ───────────────────────────────────────────────────────── */
.bl-add-boat {
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: var(--spacing-sm, 8px);
	min-height: 140px;
	padding: var(--spacing-lg, 20px);
	border: 1px dashed var(--border-color);
	border-radius: var(--radius-lg, 12px);
	background: transparent;
	color: var(--text-secondary);
	font-size: var(--font-size-sm, 0.85rem);
	cursor: pointer;
	transition: border-color 0.12s ease, color 0.12s ease;
}

.bl-add-boat:hover {
	border-color: var(--color-primary);
	color: var(--color-primary);
}

.bl-add-boat svg { width: 22px; height: 22px; }

.bl-new-boat {
	align-self: center;
	min-width: 12em;
	text-align: center;
}

/* ── Choosing which readings a card shows ────────────────────────────────── */
.bl-device-config {
	flex: none;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 24px;
	height: 24px;
	padding: 0;
	border: 0;
	border-radius: var(--radius-sm, 4px);
	background: transparent;
	color: var(--text-muted);
	cursor: pointer;
	opacity: 0;
	transition: opacity 0.12s ease, color 0.12s ease, background 0.12s ease;
}

.bl-device-config svg { width: 16px; height: 16px; }

.bl-sensor-card:hover .bl-device-config,
.bl-device-config:focus-visible { opacity: 1; }

.bl-device-config:hover {
	color: var(--color-primary);
	background: var(--accent-soft);
}

@media (hover: none) {
	.bl-device-config { opacity: 0.45; }
}

.bl-setting-head,
.bl-reading-setting {
	display: grid;
	grid-template-columns: 1fr 3.2em 4.2em;
	align-items: center;
	gap: var(--spacing-sm, 8px);
	font-size: var(--font-size-sm, 0.85rem);
}

.bl-setting-head {
	padding-bottom: 4px;
	border-bottom: 1px solid var(--border-color);
	color: var(--text-muted);
	font-size: 0.72rem;
	text-transform: uppercase;
	letter-spacing: 0.04em;
}

.bl-setting-head span:not(:first-child),
.bl-setting-box { justify-self: center; }

.bl-reading-setting {
	padding: 6px 0;
	color: var(--text-secondary);
}

.bl-setting-name {
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.bl-setting-box { cursor: pointer; }
.bl-reading-setting input[type="checkbox"] { accent-color: var(--color-primary); }

/* Limits sit under their row, spanning all three columns. */
.bl-limit-row {
	grid-column: 1 / -1;
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: var(--spacing-sm, 8px);
	padding: 2px 0 6px;
}

.bl-limit {
	display: inline-flex;
	align-items: center;
	gap: 4px;
	color: var(--text-muted);
}

.bl-limit input {
	width: 5.5em;
	padding: 2px 6px;
	border: 1px solid var(--border-color);
	border-radius: var(--radius-sm, 4px);
	background: var(--bg-primary);
	color: var(--text-primary);
	font: inherit;
}

.bl-limit-hint {
	color: var(--text-muted);
	font-size: 0.72rem;
}

/* ── Device settings modal ───────────────────────────────────────────────── */
.bl-modal-overlay {
	position: fixed;
	inset: 0;
	z-index: 60;
	display: flex;
	align-items: flex-end;
	justify-content: center;
	padding: 0;
	background: rgba(4, 16, 24, 0.72);
	overflow-y: auto;
	overscroll-behavior: contain;
}

.bl-modal-overlay[hidden] { display: none; }

.bl-modal {
	width: 100%;
	max-width: 480px;
	max-height: 92vh;
	display: flex;
	flex-direction: column;
	border: 1px solid var(--border-color);
	border-bottom: 0;
	border-radius: var(--radius-lg, 12px) var(--radius-lg, 12px) 0 0;
	background: var(--bg-card);
	box-shadow: 0 -8px 40px rgba(0, 0, 0, 0.45);
}

/* On a phone it is a sheet from the bottom, which is where a thumb is.
   With room, it centres. */
@media (min-width: 640px) {
	.bl-modal-overlay { align-items: center; padding: var(--spacing-lg, 20px); }
	.bl-modal { border-bottom: 1px solid var(--border-color); border-radius: var(--radius-lg, 12px); }
}

.bl-modal-head {
	display: flex;
	align-items: center;
	gap: var(--spacing-sm, 8px);
	padding: var(--spacing-md, 14px) var(--spacing-lg, 18px);
	border-bottom: 1px solid var(--border-color);
}

.bl-modal-title {
	flex: 1;
	min-width: 0;
	margin: 0;
	font-size: var(--font-size-lg, 1.05rem);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.bl-modal-close {
	flex: none;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 32px;
	height: 32px;
	padding: 0;
	border: 0;
	border-radius: var(--radius-sm, 4px);
	background: transparent;
	color: var(--text-secondary);
	cursor: pointer;
}

.bl-modal-close svg { width: 18px; height: 18px; }
.bl-modal-close:hover { color: var(--text-primary); background: var(--bg-tertiary); }

.bl-modal-body {
	flex: 1;
	min-height: 0;
	overflow-y: auto;
	padding: var(--spacing-lg, 18px);
	-webkit-overflow-scrolling: touch;
}

.bl-modal-section + .bl-modal-section {
	margin-top: var(--spacing-lg, 20px);
	padding-top: var(--spacing-lg, 20px);
	border-top: 1px solid var(--border-color);
}

.bl-modal-heading {
	margin: 0 0 var(--spacing-sm, 8px);
	font-size: 0.75rem;
	text-transform: uppercase;
	letter-spacing: 0.05em;
	color: var(--text-muted);
}

.bl-modal-name {
	font-size: var(--font-size-lg, 1.05rem);
	font-weight: 600;
	color: var(--text-primary);
}

.bl-modal-hint {
	margin: var(--spacing-xs, 4px) 0 var(--spacing-sm, 8px);
	font-size: var(--font-size-sm, 0.8rem);
	color: var(--text-muted);
	line-height: 1.45;
}

.bl-facts {
	display: grid;
	grid-template-columns: auto 1fr;
	gap: 2px var(--spacing-md, 12px);
	margin: var(--spacing-sm, 8px) 0 0;
	font-size: var(--font-size-sm, 0.85rem);
}

.bl-facts dt { color: var(--text-muted); }
.bl-facts dd { margin: 0; color: var(--text-secondary); overflow-wrap: anywhere; }

.bl-modal-switch {
	display: flex;
	align-items: flex-start;
	gap: var(--spacing-sm, 10px);
	padding: var(--spacing-sm, 8px) 0;
	cursor: pointer;
}

.bl-modal-switch input {
	flex: none;
	margin-top: 3px;
	accent-color: var(--color-primary);
}

.bl-modal-switch strong {
	display: block;
	font-size: var(--font-size-sm, 0.9rem);
	font-weight: 600;
	color: var(--text-primary);
}

.bl-modal-switch em {
	display: block;
	margin-top: 2px;
	font-style: normal;
	font-size: var(--font-size-sm, 0.78rem);
	color: var(--text-muted);
	line-height: 1.4;
}

.bl-modal-field {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--spacing-sm, 8px);
	padding: var(--spacing-sm, 8px) 0;
	font-size: var(--font-size-sm, 0.85rem);
	color: var(--text-secondary);
}

.bl-modal-chart {
	position: relative;
	height: 180px;
}

/* ── A sensor kept off the cards ─────────────────────────────────────────── */
.bl-hidden-row {
	display: flex;
	flex-wrap: wrap;
	gap: var(--spacing-xs, 6px);
	margin-top: var(--spacing-sm, 8px);
}

.bl-hidden-chip {
	padding: 3px 10px;
	border: 1px dashed var(--border-color);
	border-radius: var(--radius-full, 9999px);
	background: transparent;
	color: var(--text-muted);
	font-size: var(--font-size-sm, 0.78rem);
	cursor: pointer;
}

.bl-hidden-chip:hover { border-color: var(--color-primary); color: var(--color-primary); }

/* Said instead of a status pill: its quiet is deliberate, not a fault. */
.bl-sensor-off {
	flex: none;
	padding: 2px 8px;
	border-radius: var(--radius-full, 9999px);
	background: var(--bg-tertiary);
	color: var(--text-muted);
	font-size: 0.72rem;
	white-space: nowrap;
}

/* A device the catalogue does not recognise: heard, never decoded. */
.bl-modal-warn {
	margin: var(--spacing-sm, 8px) 0;
	padding: 10px 12px;
	border-radius: var(--radius-md, 8px);
	background: rgba(239, 68, 68, 0.12);
	border: 1px solid rgba(239, 68, 68, 0.30);
	color: var(--status-silent);
	font-size: var(--font-size-sm, 0.8rem);
	line-height: 1.45;
}

/* ── Archived boats ──────────────────────────────────────────────────────── */
.bl-archived {
	grid-column: 1 / -1;
	margin-top: var(--spacing-md, 12px);
	padding: var(--spacing-sm, 10px) var(--spacing-md, 14px);
	border: 1px solid var(--border-color);
	border-radius: var(--radius-md, 8px);
	background: var(--bg-secondary);
}

.bl-archived > summary {
	cursor: pointer;
	list-style: none;
	font-size: var(--font-size-sm, 0.85rem);
	color: var(--text-muted);
}

.bl-archived > summary::-webkit-details-marker { display: none; }

.bl-archived-note {
	margin: var(--spacing-sm, 8px) 0;
	font-size: var(--font-size-sm, 0.8rem);
	color: var(--text-muted);
	line-height: 1.45;
}

.bl-archived-row {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--spacing-sm, 8px);
	padding: 8px 0;
	border-top: 1px solid var(--border-color);
}

.bl-archived-name {
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
	color: var(--text-secondary);
}

.bl-archived-actions {
	flex: none;
	display: flex;
	align-items: center;
	gap: var(--spacing-sm, 10px);
}

.bl-archived-open { color: var(--color-primary); font-size: var(--font-size-sm, 0.85rem); }

.bl-archived-restore {
	padding: 4px 10px;
	border: 1px solid var(--border-color);
	border-radius: var(--radius-sm, 4px);
	background: transparent;
	color: var(--text-secondary);
	font-size: var(--font-size-sm, 0.8rem);
	cursor: pointer;
}

.bl-archived-restore:hover { border-color: var(--color-primary); color: var(--color-primary); }

.bl-archived-banner {
	display: flex;
	align-items: center;
	justify-content: space-between;
	flex-wrap: wrap;
	gap: var(--spacing-sm, 10px);
	margin-bottom: var(--spacing-md, 14px);
	padding: 10px 14px;
	border: 1px solid rgba(148, 163, 184, 0.3);
	border-radius: var(--radius-md, 8px);
	background: rgba(148, 163, 184, 0.10);
	color: var(--text-secondary);
	font-size: var(--font-size-sm, 0.85rem);
}

/* Destructive-ish, so it sits at the end of the page and stays quiet. */
.bl-boat-archive {
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: var(--spacing-sm, 10px);
	margin: var(--spacing-xl, 28px) 0 var(--spacing-lg, 20px);
	padding-top: var(--spacing-lg, 18px);
	border-top: 1px solid var(--border-color);
}

.bl-archive-boat {
	flex: none;
	padding: 6px 14px;
	border: 1px solid var(--border-color);
	border-radius: var(--radius-md, 8px);
	background: transparent;
	color: var(--text-muted);
	font-size: var(--font-size-sm, 0.85rem);
	cursor: pointer;
}

.bl-archive-boat:hover { border-color: var(--status-silent); color: var(--status-silent); }

.bl-archive-note {
	flex: 1;
	min-width: 12em;
	font-size: var(--font-size-sm, 0.78rem);
	color: var(--text-muted);
	line-height: 1.45;
}

/* ── What state the boat is in ───────────────────────────────────────────── */
.bl-mode-bar {
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: var(--spacing-sm, 10px);
	margin-bottom: var(--spacing-md, 14px);
	padding: 10px 14px;
	border: 1px solid var(--border-color);
	border-radius: var(--radius-md, 8px);
	background: var(--bg-secondary);
}

.bl-mode-label {
	flex: none;
	font-size: var(--font-size-sm, 0.85rem);
	color: var(--text-secondary);
}

.bl-mode-select {
	flex: none;
	padding: 4px 10px;
	border: 1px solid var(--border-color);
	border-radius: var(--radius-sm, 4px);
	background: var(--bg-primary);
	color: var(--text-primary);
	font: inherit;
	font-size: var(--font-size-sm, 0.85rem);
	font-weight: 600;
}

.bl-mode-hint {
	flex: 1;
	min-width: 12em;
	font-size: var(--font-size-sm, 0.78rem);
	color: var(--text-muted);
	line-height: 1.4;
}

/* Deleting is not archiving, so it does not look like it. */
.bl-archive-boat.is-delete { color: var(--status-silent); border-color: rgba(239, 68, 68, 0.35); }
.bl-archive-boat.is-delete:hover { background: rgba(239, 68, 68, 0.12); }

/* ── People ──────────────────────────────────────────────────────────────── */
.bl-people-card { margin-bottom: var(--spacing-lg, 20px); }

.bl-person {
	display: flex;
	align-items: center;
	gap: var(--spacing-sm, 8px);
	padding: 8px 0;
	border-bottom: 1px solid var(--border-color);
}

.bl-person:last-of-type { border-bottom: 0; }

.bl-person-who { flex: 1; min-width: 0; color: var(--text-primary); font-size: var(--font-size-sm, 0.9rem); }
.bl-person-meta { color: var(--text-muted); font-size: var(--font-size-sm, 0.8rem); }

.bl-person-tag {
	padding: 1px 7px;
	border-radius: var(--radius-full, 9999px);
	background: var(--bg-tertiary);
	color: var(--text-muted);
	font-size: 0.68rem;
	text-transform: uppercase;
	letter-spacing: 0.04em;
}

.bl-person-level { flex: none; max-width: 12rem; padding: 4px 8px; font-size: var(--font-size-sm, 0.8rem); }

.bl-person-remove {
	flex: none;
	padding: 4px 10px;
	border: 1px solid var(--border-color);
	border-radius: var(--radius-sm, 4px);
	background: transparent;
	color: var(--text-muted);
	font-size: var(--font-size-sm, 0.8rem);
	cursor: pointer;
}

.bl-person-remove:hover { border-color: var(--status-silent); color: var(--status-silent); }

/* Beside a name still waiting on an invitation: another way in for somebody
   whose email never arrived. */
.bl-person-link {
	flex: none;
	padding: 4px 10px;
	border: 1px solid var(--border-color);
	border-radius: var(--radius-sm, 4px);
	background: transparent;
	color: var(--text-secondary);
	font-size: var(--font-size-sm, 0.8rem);
	cursor: pointer;
}

.bl-person-link:hover { border-color: var(--color-primary); color: var(--color-primary); }
.bl-person-link:disabled { opacity: 0.6; cursor: default; }

/* The invitation link, shown once after inviting somebody. Only the hash of
   the token is stored, so this is the only moment it exists in readable form -
   which is why it gets a box of its own rather than a toast that fades. */
.bl-invite-link {
	margin: var(--spacing-md, 12px) 0 0;
	padding: var(--spacing-md, 12px);
	border: 1px solid var(--border-color);
	border-radius: var(--radius-md, 10px);
	background: var(--bg-tertiary);
}

.bl-invite-link-label {
	display: block;
	margin-bottom: 6px;
	color: var(--text-secondary);
	font-size: var(--font-size-sm, 0.82rem);
}

.bl-invite-link-row {
	display: flex;
	gap: var(--spacing-sm, 8px);
}

/* A link is long and nobody reads it - it is there to be copied, so it may
   scroll rather than push the button off the row. */
.bl-invite-link-row .form-input {
	flex: 1;
	min-width: 0;
	font-family: var(--font-mono, monospace);
	font-size: var(--font-size-sm, 0.8rem);
}

/* A secondary button is bg-tertiary, and so is this box - so without an edge
   the one control that matters here reads as a word rather than a button. */
.bl-invite-link-row .btn {
	border: 1px solid var(--border-color);
	flex: none;
}

.bl-invite-link .form-hint { margin: 8px 0 0; }

.bl-invite-row {
	display: flex;
	flex-wrap: wrap;
	gap: var(--spacing-sm, 8px);
	margin-top: var(--spacing-md, 14px);
	padding-top: var(--spacing-md, 14px);
	border-top: 1px solid var(--border-color);
}

.bl-invite-email { flex: 1; min-width: 14rem; }
.bl-invite-level { flex: none; max-width: 12rem; }

/* ── A control card that cannot be operated ──────────────────────────────────
   One state for the whole card rather than each control dimming itself. A card
   that is partly bright and partly dim reads as broken; a card that is plainly
   grey, with one line saying why, reads as locked. */
.bl-locked {
	position: relative;
	border-color: var(--border-color);
	background: var(--bg-secondary);
}

/* Everything except the reason and the card title, which have to stay legible
   for the card to explain itself. */
.bl-locked .bl-switch-grid,
.bl-locked .bl-heater-controls,
.bl-locked .bl-heater-metrics,
.bl-locked .bl-heater-setpoints,
.bl-locked .bl-sensor-foot,
.bl-locked .bl-switch-status {
	opacity: 0.4;
	filter: grayscale(1);
	pointer-events: none;
	user-select: none;
}

.bl-locked .card-title { color: var(--text-secondary); }

.bl-locked-note {
	margin-top: var(--spacing-sm, 8px);
	padding: 8px 12px;
	border-radius: var(--radius-md, 8px);
	background: rgba(148, 163, 184, 0.12);
	border: 1px solid rgba(148, 163, 184, 0.28);
	color: var(--text-secondary);
	font-size: var(--font-size-sm, 0.82rem);
	line-height: 1.45;
}

/* The refresh button stays usable: asking the boat to look again is exactly
   what somebody wants when a panel is unreachable. */
.bl-locked .bl-switch-refresh {
	opacity: 1;
	filter: none;
	pointer-events: auto;
}

/* The answer before the controls: what this boat watches, and what is wrong
   with it now. A page that opens with twenty-five checkboxes has buried it. */
.bl-alert-summary {
	margin: 0 0 var(--spacing-md, 14px);
	color: var(--text-secondary);
	font-size: var(--font-size-sm, 0.85rem);
	line-height: 1.6;
}

.bl-alert-summary b { color: var(--text-primary); }

.bl-alert-firing-now {
	display: inline-block;
	margin: 0 4px;
	padding: 1px 8px;
	border-radius: 999px;
	background: rgba(239, 68, 68, 0.14);
	color: var(--status-silent);
	font-weight: 600;
}

/* A sensor folds away. Twenty-five readings laid flat is a wall; the one you
   came to change is behind its own name. */
.bl-alert-device { border-top: 1px solid var(--border-color); }
.bl-alert-device[open] { padding-bottom: var(--spacing-sm, 10px); }

.bl-alert-device-head {
	display: flex;
	align-items: baseline;
	gap: 8px;
	flex-wrap: wrap;
	padding: 10px 0;
	cursor: pointer;
	list-style: none;
}

.bl-alert-device-head::-webkit-details-marker { display: none; }

/* A disclosure triangle that turns, rather than the browser's own. */
.bl-alert-device-head::before {
	content: '';
	flex: none;
	width: 0;
	height: 0;
	border-left: 5px solid currentColor;
	border-top: 4px solid transparent;
	border-bottom: 4px solid transparent;
	color: var(--text-secondary);
	transition: transform 0.15s ease;
}

.bl-alert-device[open] > .bl-alert-device-head::before { transform: rotate(90deg); }
.bl-alert-device-head:hover { color: var(--text-primary); }

.bl-alert-device-count {
	margin-left: auto;
	color: var(--text-secondary);
	font-size: var(--font-size-sm, 0.78rem);
}

/* What this sensor is raising right now, so this page and the other tab are
   plainly about the same boat. */
.bl-alert-firing {
	padding: 1px 8px;
	border-radius: 999px;
	background: rgba(239, 68, 68, 0.14);
	color: var(--status-silent);
	font-size: var(--font-size-sm, 0.75rem);
	font-weight: 600;
}

/* Where a boat's alerts go. Sits above the rules, because "what counts as an
   alert" is only worth setting if one can reach somebody. */
.bl-alert-delivery {
	margin: 0 0 var(--spacing-md, 14px);
	padding: var(--spacing-sm, 10px) var(--spacing-md, 14px);
	border: 1px solid var(--border-color);
	border-radius: var(--radius-md, 8px);
	background: var(--bg-secondary);
}

.bl-alert-delivery-head {
	margin-bottom: 6px;
	font-size: var(--font-size-sm, 0.82rem);
	font-weight: 600;
	letter-spacing: 0.03em;
	text-transform: uppercase;
	color: var(--text-secondary);
}

.bl-alert-channel {
	display: flex;
	align-items: center;
	gap: 8px;
	flex-wrap: wrap;
	padding: 6px 0;
}

.bl-alert-channel-name { flex: 1; font-size: var(--font-size-sm, 0.9rem); }

.bl-alert-channel select {
	padding: 4px 8px;
	border: 1px solid var(--border-color);
	border-radius: var(--radius-sm, 6px);
	background: var(--bg-card);
	color: var(--text-primary);
	font-size: var(--font-size-sm, 0.8rem);
}

.bl-alert-channel-remove,
.bl-alert-connect {
	padding: 5px 10px;
	border: 1px solid var(--border-color);
	border-radius: var(--radius-sm, 6px);
	background: none;
	color: var(--text-secondary);
	font-size: var(--font-size-sm, 0.8rem);
	cursor: pointer;
}

.bl-alert-connect { margin-top: 6px; border-style: dashed; width: 100%; }
.bl-alert-channel-remove:hover,
.bl-alert-connect:hover { color: var(--text-primary); background: var(--bg-tertiary); border-style: solid; }

.bl-alert-link { margin-top: 8px; font-size: var(--font-size-sm, 0.82rem); }
.bl-alert-link[hidden] { display: none; }

/* The code is meant to be typed into a phone, so it is big enough to read
   across a desk. */
.bl-alert-code {
	display: inline-block;
	margin: 4px 0;
	padding: 6px 12px;
	border-radius: var(--radius-sm, 6px);
	background: var(--bg-tertiary);
	font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
	font-size: 1rem;
	letter-spacing: 0.08em;
}

/* ── Where a boat is ─────────────────────────────────────────────────────────
   Folded away by default: the position matters when you are wondering about it
   and takes a screenful when you are not. */
/* ── Sub header: which view of this page you are on ──────────────────────────
   One component, two users - the boat's views and the alerts page's two lists.
   Written as one rule rather than two lookalikes, because a sub header that
   drifts between pages is worse than one that was never shared. */
.bl-subnav,
.bl-alert-tabs {
	display: flex;
	gap: 4px;
	margin-bottom: var(--spacing-lg, 20px);
	border-bottom: 1px solid var(--border-color);
	overflow-x: auto;
	scrollbar-width: none;
}

.bl-subnav::-webkit-scrollbar,
.bl-alert-tabs::-webkit-scrollbar { display: none; }

.bl-subnav-item,
.bl-alert-tab {
	flex: none;
	padding: 10px 14px;
	border: 0;
	border-bottom: 2px solid transparent;
	margin-bottom: -1px;
	background: none;
	color: var(--text-secondary);
	font-size: var(--font-size-sm, 0.9rem);
	font-weight: 500;
	white-space: nowrap;
	cursor: pointer;
}

.bl-subnav-item:hover,
.bl-alert-tab:hover { color: var(--text-primary); }

.bl-subnav-item.active,
.bl-alert-tab.active {
	color: var(--text-primary);
	border-bottom-color: var(--accent, #10b981);
}

.bl-subnav-item:focus-visible {
	outline: 2px solid var(--accent, #10b981);
	outline-offset: 3px;
	border-radius: var(--radius-sm, 6px);
}

/* A hidden view has to be gone, not merely invisible: Leaflet measures zero in
   a container that is still laid out, and draws one tile in the corner. */
.bl-tab-panel[hidden] { display: none; }

/* ============ ARRANGING THE PAGE ============
   See layout.js. Cards are ordered with flexbox `order` rather than by being
   moved, so a re-render - which happens on every reading that arrives - cannot
   undo somebody's arrangement. A column of full-width children lays out the
   same as the block flow it replaces. */

.bl-tab-panel {
	display: flex;
	flex-direction: column;
	align-items: stretch;
}

/* The box compartments render into stops being a layout box, so each
   compartment becomes a card of the tab itself - which is what lets one be
   moved above the History chart rather than only among its siblings. */
.bl-block-passthrough { display: contents; }

/* The spinner while the boat loads, and the "nothing aboard yet" notice. Not
   cards, but they occupy the place cards will: after them, before the actions. */
.bl-block-passthrough > *:not([data-block]) { order: 500; }

/* Actions are not cards. Whatever has been arranged above them, "Add a device"
   is at the bottom of it. */
.bl-block-actions { order: 9999; }

.bl-block-hidden { display: none; }

/* The row carrying the tabs and the one control that outlives them. */
.bl-subnav-row {
	display: flex;
	align-items: flex-end;
	gap: var(--spacing-sm, 8px);
}

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

.bl-arrange-btn {
	flex: none;
	margin-bottom: var(--spacing-lg, 20px);
	padding: 6px 12px;
	border: 1px solid var(--border-color);
	border-radius: var(--radius-md, 8px);
	background: transparent;
	color: var(--text-secondary);
	font-size: var(--font-size-sm, 0.85rem);
	font-weight: 500;
	cursor: pointer;
	transition: all var(--transition-fast, 0.15s);
}

.bl-arrange-btn:hover { color: var(--text-primary); border-color: var(--text-muted); }

.bl-arrange-btn.is-active {
	background: var(--color-primary, #10b981);
	border-color: var(--color-primary, #10b981);
	color: #fff;
}

.bl-arrange-hint {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--spacing-sm, 8px);
	margin-bottom: var(--spacing-lg, 20px);
	padding: var(--spacing-sm, 8px) var(--spacing-md, 12px);
	border: 1px dashed var(--border-color);
	border-radius: var(--radius-md, 8px);
	color: var(--text-secondary);
	font-size: var(--font-size-sm, 0.85rem);
}

.bl-arrange-hint span { flex: 1; min-width: 220px; }

.bl-arrange-reset {
	flex: none;
	padding: 0;
	border: 0;
	background: none;
	color: var(--text-muted);
	font-size: var(--font-size-sm, 0.85rem);
	text-decoration: underline;
	cursor: pointer;
}

.bl-arrange-reset:hover { color: var(--text-primary); }

/* The handle strip above each card while arranging. A sibling of its card, not
   a child - half these cards have their innards replaced by a socket message,
   and a handle living inside one would be thrown away by a redraw. */
.bl-block-bar {
	display: flex;
	align-items: center;
	gap: var(--spacing-xs, 4px);
	margin-bottom: 4px;
	padding: 4px 8px;
	border: 1px solid var(--border-color);
	border-radius: var(--radius-md, 8px);
	background: var(--bg-tertiary);
	cursor: grab;
}

.bl-block-bar.is-dragging { opacity: 0.5; cursor: grabbing; }
.bl-block-bar.is-drop-target { border-color: var(--color-primary, #10b981); }

.bl-block-grip { display: inline-flex; color: var(--text-muted); }
.bl-block-grip svg { width: 16px; height: 16px; }

.bl-block-name {
	flex: 1;
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
	color: var(--text-primary);
	font-size: var(--font-size-sm, 0.85rem);
	font-weight: 600;
}

.bl-block-tag {
	flex: none;
	padding: 1px 6px;
	border-radius: var(--radius-sm, 4px);
	background: var(--bg-secondary);
	color: var(--text-muted);
	font-size: var(--font-size-xs, 0.72rem);
}

.bl-block-btn {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 28px;
	height: 28px;
	padding: 0;
	border: 0;
	border-radius: var(--radius-sm, 4px);
	background: transparent;
	color: var(--text-secondary);
	cursor: pointer;
}

.bl-block-btn svg { width: 16px; height: 16px; }
.bl-block-btn:hover:not([disabled]) { color: var(--text-primary); background: var(--bg-secondary); }
.bl-block-btn[disabled] { opacity: 0.3; cursor: default; }

/* While arranging, a hidden card is shown faintly rather than removed: a card
   you cannot see is a card you cannot bring back. */
.bl-arranging .bl-block-hidden { display: block; opacity: 0.4; }

.bl-hidden-note {
	margin: 0 0 var(--spacing-lg, 20px);
	color: var(--text-muted);
	font-size: var(--font-size-sm, 0.85rem);
}

.bl-hidden-note-link {
	padding: 0;
	border: 0;
	background: none;
	color: var(--color-primary, #10b981);
	font-size: inherit;
	text-decoration: underline;
	cursor: pointer;
}

@media (max-width: 768px) {
	/* Dragging is a mouse gesture. On a phone the arrows are the control, and a
	   grip would only invite a drag that scrolls the page instead. */
	.bl-block-grip { display: none; }
	.bl-block-btn { width: 36px; height: 36px; }
	.bl-arrange-btn { padding: 8px 12px; }
}

.bl-map-card { margin-bottom: var(--spacing-lg, 20px); padding: 0; overflow: hidden; }

/* The map is a page of its own, so it runs to the bottom of the screen rather
   than sitting in a card with room left over. The height below is only what it
   uses before the exact one is measured - see sizeMap() in boat.js. */
#boat-tab-map { margin-bottom: 0; }
#boat-tab-map .bl-map-card { margin-bottom: 0; }
#boat-tab-map .bl-map { height: clamp(320px, 60vh, 640px); }

/* Something not built yet, said plainly. */
.bl-soon { padding: var(--spacing-xl, 26px); max-width: 62ch; }

.bl-soon p { margin: 0 0 10px; color: var(--text-secondary); line-height: 1.6; }

.bl-soon .bl-soon-note { margin-bottom: 0; color: var(--text-muted); font-size: var(--font-size-sm, 0.85rem); }

/* ── The anchor watch ────────────────────────────────────────────────────────
   A bar above the map, because the circle it sets is drawn on the map right
   below it. Quiet until it has something to say, then loud. */
.bl-anchor {
	display: flex;
	align-items: center;
	padding: 10px var(--spacing-lg, 18px);
	border-bottom: 1px solid var(--border-color);
	background: var(--bg-secondary);
}

.bl-anchor-row {
	display: flex;
	align-items: center;
	gap: var(--spacing-md, 14px);
	flex-wrap: wrap;
	width: 100%;
	font-size: var(--font-size-sm, 0.85rem);
}

.bl-anchor-state { font-weight: 600; color: var(--text-secondary); }
.bl-anchor-state.is-set { color: var(--accent, #10b981); }
.bl-anchor-state.is-warn { color: var(--status-late, #f59e0b); }
.bl-anchor-state.is-alarm { color: var(--color-danger, #ef4444); }

.bl-anchor-note { color: var(--text-muted); }

.bl-anchor-distance { color: var(--text-secondary); font-variant-numeric: tabular-nums; }
.bl-anchor-distance.is-out { color: var(--color-danger, #ef4444); font-weight: 600; }

.bl-anchor-radius { display: flex; align-items: center; gap: 6px; color: var(--text-muted); }

.bl-anchor-radius input {
	width: 64px;
	padding: 4px 8px;
	border: 1px solid var(--border-color);
	border-radius: var(--radius-sm, 6px);
	background: var(--bg-card);
	color: var(--text-primary);
	font: inherit;
	font-size: var(--font-size-sm, 0.85rem);
}

.bl-anchor-drop { margin-left: auto; }

/* The three actions on a watch that already exists. Links rather than buttons:
   one primary action per bar, and this bar's was spent dropping it. */
.bl-anchor-link {
	border: 0;
	background: none;
	padding: 0;
	color: var(--accent, #10b981);
	font: inherit;
	font-size: var(--font-size-sm, 0.85rem);
	cursor: pointer;
}

.bl-anchor-link:hover { text-decoration: underline; }
.bl-anchor-link.is-quiet { color: var(--text-muted); }
.bl-anchor-link.is-quiet:hover { color: var(--color-danger, #ef4444); }

.bl-anchor-pin {
	display: block;
	font-size: 15px;
	line-height: 18px;
	text-align: center;
	text-shadow: 0 0 3px rgba(0, 0, 0, 0.9), 0 0 6px rgba(0, 0, 0, 0.6);
}

@media (max-width: 640px) {
	.bl-anchor-drop { margin-left: 0; }
}

/* ── Passages, under the map ─────────────────────────────────────────────────
   A column of trips: when, where to where, how far, how long. Rows rather than
   cards - the eye runs down a list of dates far better than it scans a grid,
   and this is a thing you skim to find one. */
.bl-passage-head {
	display: flex;
	align-items: baseline;
	gap: 8px;
	margin: var(--spacing-lg, 18px) 0 8px;
}

.bl-passage-title { font-weight: 600; color: var(--text-primary); }

.bl-passage-count {
	color: var(--text-muted);
	font-size: var(--font-size-sm, 0.82rem);
	font-variant-numeric: tabular-nums;
}

.bl-passage-list {
	display: flex;
	flex-direction: column;
	border: 1px solid var(--border-color);
	border-radius: var(--radius-md, 10px);
	overflow: hidden;
	background: var(--bg-card);
}

.bl-passage-row {
	display: grid;
	grid-template-columns: 76px 1fr auto auto;
	align-items: baseline;
	gap: var(--spacing-md, 14px);
	width: 100%;
	padding: 10px var(--spacing-lg, 16px);
	border: 0;
	border-bottom: 1px solid var(--border-color);
	background: none;
	color: var(--text-secondary);
	font: inherit;
	font-size: var(--font-size-sm, 0.86rem);
	text-align: left;
	cursor: pointer;
	transition: background var(--transition-fast);
}

.bl-passage-row:last-child { border-bottom: 0; }
.bl-passage-row:hover { background: var(--bg-tertiary); }

/* The one on the map. Marked on the left edge rather than by colouring the
   whole row, which at this density reads as an alert. */
.bl-passage-row.active {
	background: var(--bg-tertiary);
	box-shadow: inset 3px 0 0 var(--accent, #10b981);
}

.bl-passage-when { color: var(--text-muted); font-variant-numeric: tabular-nums; }
.bl-passage-where { color: var(--text-primary); min-width: 0; }

.bl-passage-live {
	margin-left: 8px;
	padding: 1px 6px;
	border-radius: 999px;
	background: rgba(16, 185, 129, 0.14);
	color: var(--accent, #10b981);
	font-size: 0.7rem;
	text-transform: uppercase;
	letter-spacing: 0.04em;
}

.bl-passage-far,
.bl-passage-took { font-variant-numeric: tabular-nums; white-space: nowrap; }
.bl-passage-took { color: var(--text-muted); }

.bl-passage-none {
	margin-top: var(--spacing-lg, 18px);
	padding: var(--spacing-md, 14px) var(--spacing-lg, 16px);
	border: 1px solid var(--border-color);
	border-radius: var(--radius-md, 10px);
	color: var(--text-muted);
	font-size: var(--font-size-sm, 0.85rem);
	line-height: 1.5;
}

@media (max-width: 640px) {
	/* Four columns will not fit a phone: the distance and the time drop under
	   the route rather than being squeezed into slivers. */
	.bl-passage-row {
		grid-template-columns: 64px 1fr auto;
		row-gap: 2px;
	}
	.bl-passage-took { grid-column: 2 / 4; color: var(--text-muted); }
}

/* ── The logbook ─────────────────────────────────────────────────────────────
   Read as a column of dated facts, so the date and what kind of thing it was
   sit in their own gutter and the eye can run down it without reading. */
.bl-log-head {
	display: flex;
	align-items: center;
	gap: var(--spacing-md, 14px);
	flex-wrap: wrap;
	margin-bottom: var(--spacing-lg, 18px);
}

.bl-log-total {
	color: var(--text-muted);
	font-size: var(--font-size-sm, 0.82rem);
	font-variant-numeric: tabular-nums;
}

/* The buttons sit together at the right, so the filters keep the left edge and
   the eye finds "what can I do here" in one place. */
.bl-log-head-actions {
	margin-left: auto;
	display: flex;
	align-items: center;
	gap: var(--spacing-sm, 8px);
	flex-wrap: wrap;
}

.bl-log-add { margin-left: 0; }

/* ── The checklist ───────────────────────────────────────────────────────────
   Above the history, because it is the only part of the page that asks for
   anything. A tick writes a logbook entry, so the control is a real target
   rather than a line of text. */
.bl-todo {
	padding: var(--spacing-md, 14px);
	margin-bottom: var(--spacing-lg, 18px);
}

.bl-todo-head {
	display: flex;
	align-items: center;
	gap: var(--spacing-sm, 8px);
	flex-wrap: wrap;
	margin-bottom: var(--spacing-sm, 8px);
}
.bl-todo-head .bl-due-head { margin: 0; }
.bl-todo-actions { margin-left: auto; display: flex; gap: 0.4rem; flex-wrap: wrap; }

.bl-todo-empty {
	color: var(--text-muted);
	font-size: var(--font-size-sm, 0.82rem);
	line-height: 1.55;
	margin: 0;
}

.bl-todo-list { list-style: none; margin: 0; padding: 0; }

.bl-todo-item {
	display: flex;
	align-items: center;
	gap: 0.7rem;
	padding: 0.5rem 0;
	border-bottom: 1px solid var(--border-color);
}
.bl-todo-item:last-child { border-bottom: 0; }

/* Big enough to hit on a phone with wet hands, which is where this gets used. */
.bl-todo-tick {
	flex: 0 0 22px;
	width: 22px;
	height: 22px;
	border-radius: 6px;
	border: 2px solid var(--border-color);
	background: transparent;
	cursor: pointer;
	position: relative;
}
.bl-todo-tick:hover { border-color: #34d399; }
.bl-todo-tick::after {
	content: '';
	position: absolute;
	left: 6px; top: 2px;
	width: 5px; height: 10px;
	border: solid #34d399;
	border-width: 0 2px 2px 0;
	transform: rotate(45deg);
	opacity: 0;
}
.bl-todo-tick:hover::after { opacity: 1; }
.bl-todo-tick.is-readonly { cursor: default; opacity: 0.4; }

.bl-todo-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.1rem; }
.bl-todo-title { color: var(--text-primary); font-weight: 550; }

.bl-todo-when {
	font-size: var(--font-size-sm, 0.82rem);
	color: var(--text-muted);
	white-space: nowrap;
	font-variant-numeric: tabular-nums;
}
/* Overdue says so in words as well - never colour alone. */
.bl-todo-item.is-overdue .bl-todo-when { color: #f87171; font-weight: 600; }
.bl-todo-item.is-overdue { background: rgb(239 68 68 / 0.05); }
/* A job with no date is waiting, not late. It should not look like either the
   overdue ones above it or the scheduled ones below. */
.bl-todo-item.is-undated .bl-todo-when { font-style: italic; }

.bl-todo-form { margin-bottom: var(--spacing-md, 14px); }
.bl-todo-hint {
	font-size: 0.72rem;
	color: var(--text-muted);
	margin: 0.2rem 0 0;
}

/* ── The usual jobs ───────────────────────────────────────────────────────── */

.bl-presets {
	margin-bottom: var(--spacing-md, 14px);
	padding: var(--spacing-md, 14px);
	background: var(--bg-tertiary, rgb(255 255 255 / 0.03));
	border: 1px solid var(--border-color);
	border-radius: var(--radius-md, 10px);
}

.bl-preset-intro {
	margin: 0 0 var(--spacing-sm, 8px);
	font-size: 0.75rem;
	color: var(--text-muted);
	line-height: 1.5;
}

.bl-preset-group + .bl-preset-group { margin-top: var(--spacing-sm, 8px); }
.bl-preset-group h4 {
	margin: 0 0 0.3rem;
	font-size: 0.7rem;
	text-transform: uppercase;
	letter-spacing: 0.05em;
	color: var(--text-muted);
	font-weight: 600;
}

.bl-preset-jobs { display: flex; flex-wrap: wrap; gap: 0.35rem; }

.bl-preset-job {
	display: inline-flex;
	align-items: baseline;
	gap: 0.35rem;
	padding: 0.3rem 0.6rem;
	font-size: 0.78rem;
	border-radius: var(--radius-full, 999px);
	border: 1px solid var(--border-color);
	background: var(--bg-secondary, transparent);
	color: var(--text-primary);
	cursor: pointer;
}
.bl-preset-job:hover { border-color: #6366f1; color: #a5b4fc; }
.bl-preset-job em { font-style: normal; color: var(--text-muted); font-size: 0.7rem; }

/* Already on the list: shown rather than hidden, so the catalogue does not
   appear to lose items, and not offered twice. */
.bl-preset-job.is-taken {
	cursor: default;
	opacity: 0.45;
	text-decoration: line-through;
}

@media (max-width: 560px) {
	.bl-todo-item { flex-wrap: wrap; }
	.bl-todo-when { width: 100%; padding-left: 1.9rem; }
	.bl-todo-actions { width: 100%; margin-left: 0; }
}

/* ── Maintenance ─────────────────────────────────────────────────────────────
   The only part of a logbook that is about the future, so it sits above the
   history rather than inside it. Hidden entirely until a recurring job exists:
   an empty schedule would be furniture explaining a feature nobody asked for. */
.bl-due {
	padding: var(--spacing-md, 14px);
	margin-bottom: var(--spacing-lg, 18px);
}

.bl-due-head {
	display: flex;
	align-items: center;
	gap: var(--spacing-sm, 8px);
	margin: 0 0 var(--spacing-sm, 8px);
	font-size: var(--font-size-sm, 0.82rem);
	text-transform: uppercase;
	letter-spacing: 0.05em;
	color: var(--text-muted);
}

.bl-due-count {
	background: rgb(239 68 68 / 0.14);
	color: #f87171;
	border-radius: var(--radius-full, 999px);
	padding: 0.1rem 0.5rem;
	font-size: 0.7rem;
	letter-spacing: 0;
	text-transform: none;
}

.bl-due-list { list-style: none; margin: 0; padding: 0; }

.bl-due-item {
	display: flex;
	align-items: baseline;
	gap: var(--spacing-md, 14px);
	padding: 0.45rem 0;
	border-bottom: 1px solid var(--border-color);
}
.bl-due-item:last-child { border-bottom: 0; }

/* The interval in its own gutter, matching how a dated entry reads below. */
.bl-due-when {
	flex: 0 0 8.5rem;
	font-size: var(--font-size-sm, 0.82rem);
	color: var(--text-muted);
	font-variant-numeric: tabular-nums;
}

/* Overdue is stated in words as well as coloured - never colour alone. */
.bl-due-item.is-overdue .bl-due-when { color: #f87171; font-weight: 600; }

.bl-due-what { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }

@media (max-width: 560px) {
	.bl-due-item { flex-direction: column; gap: 0.15rem; }
	.bl-due-when { flex: none; }
}

.bl-log-entry {
	display: flex;
	gap: var(--spacing-lg, 18px);
	padding: var(--spacing-md, 14px) 0;
	border-bottom: 1px solid var(--border-color);
}

.bl-log-entry:last-child { border-bottom: 0; }

.bl-log-when {
	display: flex;
	flex-direction: column;
	gap: 4px;
	flex: none;
	width: 108px;
}

.bl-log-date {
	color: var(--text-secondary);
	font-size: var(--font-size-sm, 0.82rem);
	font-variant-numeric: tabular-nums;
}

/* The kind, as a quiet label rather than a badge: on a page that is mostly
   repairs, six coloured pills would be decoration, not information. */
.bl-log-kind {
	color: var(--text-muted);
	font-size: 0.7rem;
	letter-spacing: 0.04em;
	text-transform: uppercase;
}

.bl-log-kind[data-kind="fault"] { color: var(--color-danger, #ef4444); }
.bl-log-kind[data-kind="repair"] { color: var(--accent, #10b981); }

.bl-log-main { flex: 1; min-width: 0; }

.bl-log-title {
	display: flex;
	align-items: baseline;
	gap: var(--spacing-sm, 10px);
	font-weight: 600;
	color: var(--text-primary);
}

.bl-log-cost {
	margin-left: auto;
	flex: none;
	color: var(--text-secondary);
	font-weight: 500;
	font-variant-numeric: tabular-nums;
}

/* Kept as typed: line breaks are how somebody writes down three things they
   did, and running them together loses the list. */
.bl-log-body {
	margin: 6px 0 0;
	color: var(--text-secondary);
	line-height: 1.55;
	white-space: pre-wrap;
}

.bl-log-facts {
	display: flex;
	align-items: center;
	gap: 8px;
	flex-wrap: wrap;
	margin-top: 8px;
	color: var(--text-muted);
	font-size: var(--font-size-sm, 0.8rem);
}

.bl-log-dot { opacity: 0.5; }

.bl-log-remove {
	flex: none;
	align-self: flex-start;
	width: 26px;
	height: 26px;
	border: 0;
	border-radius: var(--radius-sm, 6px);
	background: none;
	color: var(--text-muted);
	font-size: 1.1rem;
	line-height: 1;
	cursor: pointer;
	opacity: 0;
	transition: opacity var(--transition-fast), color var(--transition-fast);
}

.bl-log-entry:hover .bl-log-remove,
.bl-log-remove:focus-visible { opacity: 1; }

.bl-log-remove:hover { color: var(--color-danger, #ef4444); }

.bl-log-form {
	display: flex;
	flex-direction: column;
	gap: var(--spacing-md, 14px);
	padding: var(--spacing-lg, 18px);
	margin-bottom: var(--spacing-lg, 18px);
}

.bl-log-form label {
	display: flex;
	flex-direction: column;
	gap: 5px;
	flex: 1;
	min-width: 160px;
	color: var(--text-secondary);
	font-size: var(--font-size-sm, 0.82rem);
}

.bl-log-form-row {
	display: flex;
	gap: var(--spacing-md, 14px);
	flex-wrap: wrap;
}

.bl-log-form .form-input,
.bl-log-form .form-select {
	width: 100%;
	padding: var(--spacing-xs, 6px) var(--spacing-sm, 10px);
	border: 1px solid var(--border-color);
	border-radius: var(--radius-md, 8px);
	background: var(--bg-tertiary);
	color: var(--text-primary);
	font: inherit;
	font-size: var(--font-size-sm, 0.88rem);
}

.bl-log-form textarea.form-input { resize: vertical; }

.bl-log-form-actions { display: flex; gap: var(--spacing-sm, 10px); }

@media (max-width: 640px) {
	/* The date gutter costs a third of a phone screen, so on one the date
	   leads the entry instead of standing beside it. */
	.bl-log-entry { flex-direction: column; gap: 6px; }
	.bl-log-when { flex-direction: row; align-items: baseline; gap: 10px; width: auto; }
	.bl-log-remove { opacity: 1; position: absolute; right: 0; }
	.bl-log-entry { position: relative; }
	.bl-log-add { margin-left: 0; }
}

.bl-map-head {
	display: flex;
	align-items: baseline;
	gap: 10px;
	flex-wrap: wrap;
	padding: var(--spacing-md, 14px) var(--spacing-lg, 18px);
	list-style: none;
}

.bl-map-head::-webkit-details-marker { display: none; }

/* Only the fleet map still folds away. The boat's map has a tab to itself, so
   its head is a title rather than a control - and must not wear a disclosure
   triangle that does nothing. */
details.bl-map-card > .bl-map-head { cursor: pointer; }

details.bl-map-card > .bl-map-head::before {
	content: '';
	flex: none;
	width: 0;
	height: 0;
	border-left: 5px solid currentColor;
	border-top: 4px solid transparent;
	border-bottom: 4px solid transparent;
	color: var(--text-secondary);
	transition: transform 0.15s ease;
}

.bl-map-card[open] > .bl-map-head::before { transform: rotate(90deg); }

.bl-map-title { font-weight: 600; }

/* The place name, and the coordinates behind it. "Espoo" is the answer; the
   numbers are the evidence. */
.bl-map-place {
	margin-left: auto;
	color: var(--text-secondary);
	font-size: var(--font-size-sm, 0.82rem);
	font-variant-numeric: tabular-nums;
}

.bl-map {
	height: 340px;
	width: 100%;
	background: var(--bg-secondary);
}

/* Leaflet paints its own white, which in a slate-dark app is a hole in the
   page. Everything below puts the map inside the design rather than on top of
   it. */
.leaflet-container {
	background: var(--bg-primary);
	font: inherit;
	color: var(--text-primary);
}

/* The basemap, darkened.
   Standard OSM tiles rather than a ready-made dark basemap: the dark ones are
   all somebody's commercial tier behind an API key, and that licensing question
   is not worth answering for a colour scheme. Inverting and rotating the hue
   turns the map dark while keeping water blue-ish and land grey. The result is
   approximated rather than designed - the honest cost of not taking on a
   provider. Only this pane is filtered, so the seamarks above it stay their own
   colours. */
.leaflet-basemap-pane {
	/* Dimmed rather than inverted. Inverting turns forest into army green and
	   the whole map reads as night vision - it looks processed, not dark. This
	   keeps the map's own colours and takes the glare off, which is what a dark
	   page actually needs from it. */
	filter: brightness(0.52) saturate(0.55) contrast(1.12);
}

/* Controls, in the app's own materials rather than Leaflet's white chrome. */
.leaflet-bar,
.leaflet-control-layers,
.leaflet-control-attribution {
	background: var(--bg-card) !important;
	color: var(--text-secondary) !important;
	border: 1px solid var(--border-color) !important;
	box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35) !important;
}

.leaflet-bar a,
.leaflet-bar a:hover {
	background: var(--bg-card);
	color: var(--text-primary);
	border-bottom-color: var(--border-color);
}

.leaflet-bar a:hover { background: var(--bg-tertiary); }

/* Full screen: the map is given the whole display, so it has to fill it. The
   height set for the page layout would otherwise leave it letterboxed in the
   middle of a black screen. */
.bl-map:fullscreen,
.bl-map-card:fullscreen {
	width: 100%;
	height: 100%;
	max-height: none;
	border-radius: 0;
}

.bl-map-card:fullscreen .bl-map { height: 100%; }
.bl-map-card:fullscreen .bl-map-head,
.bl-map-card:fullscreen .bl-map-foot,
.bl-map-card:fullscreen .bl-anchor { display: none; }

/* Buttons added to the map - "centre on the boat" - in Leaflet's own bar so
   they sit with the zoom controls instead of beside them looking foreign. */
.bl-map-btn a {
	display: flex !important;
	align-items: center;
	justify-content: center;
}

.bl-map-btn a svg { display: block; }

.leaflet-control-attribution { font-size: 0.68rem; padding: 2px 6px; }
.leaflet-control-attribution a { color: var(--text-secondary); }

.leaflet-control-scale-line {
	background: rgba(15, 23, 42, 0.7);
	border-color: var(--border-color);
	color: var(--text-secondary);
}

/* A boat, as a dot that says how it is doing - the same colours as its status
   pill, so green here and green there mean the same thing. */
.bl-pin {
	display: block;
	border-radius: 50%;
	background: var(--pin, #10b981);
	border: 2px solid rgba(15, 23, 42, 0.85);
	box-shadow: 0 0 0 1px var(--pin, #10b981), 0 2px 6px rgba(0, 0, 0, 0.5);
}

/* Only the boat's current position pulses. A track's start is a fact, not a
   thing that is still happening. */
.bl-pin-live { animation: bl-pin-pulse 2.6s ease-out infinite; }

@keyframes bl-pin-pulse {
	0%   { box-shadow: 0 0 0 1px var(--pin), 0 0 0 0 color-mix(in srgb, var(--pin) 55%, transparent); }
	70%  { box-shadow: 0 0 0 1px var(--pin), 0 0 0 14px transparent; }
	100% { box-shadow: 0 0 0 1px var(--pin), 0 0 0 0 transparent; }
}

@media (prefers-reduced-motion: reduce) {
	.bl-pin-live { animation: none; }
}

/* Popups, in the app's card material. */
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
	background: var(--bg-card);
	color: var(--text-primary);
	border: 1px solid var(--border-color);
	box-shadow: 0 8px 30px rgba(0, 0, 0, 0.45);
}

.leaflet-popup-content { font: inherit; line-height: 1.5; margin: 10px 14px; }
.leaflet-popup-close-button { color: var(--text-secondary) !important; }

.bl-pop { min-width: 130px; }
.bl-pop-place { color: var(--text-secondary); }
/* The number somebody clicked the water to find out. */
.bl-pop-depth {
	margin: 2px 0 1px;
	color: var(--text-primary);
	font-size: 1.05rem;
	font-weight: 600;
	font-variant-numeric: tabular-nums;
}

.bl-pop-note, .bl-pop-when {
	margin-top: 4px;
	color: var(--text-muted);
	font-size: var(--font-size-sm, 0.78rem);
}
.bl-pop-alert {
	margin-top: 4px;
	color: var(--status-silent, #ef4444);
	font-weight: 600;
	font-size: var(--font-size-sm, 0.8rem);
}
.bl-pop-link { margin-top: 6px; }
.bl-pop-link a { color: var(--color-primary, #4f46e5); font-weight: 600; }

/* The layer toggle, which is otherwise white-on-white. */
.leaflet-control-layers-expanded { padding: 8px 10px; color: var(--text-primary) !important; }
.leaflet-control-layers-selector { margin-right: 6px; }

.bl-map-foot {
	display: flex;
	align-items: center;
	gap: var(--spacing-md, 14px);
	flex-wrap: wrap;
	padding: 10px var(--spacing-lg, 18px);
	border-top: 1px solid var(--border-color);
	color: var(--text-secondary);
	font-size: var(--font-size-sm, 0.82rem);
}

.bl-map-foot-label {
	color: var(--text-muted);
	font-size: var(--font-size-sm, 0.82rem);
}

.bl-map-foot select {
	padding: 4px 8px;
	border: 1px solid var(--border-color);
	border-radius: var(--radius-sm, 6px);
	background: var(--bg-card);
	color: var(--text-primary);
	font-size: var(--font-size-sm, 0.8rem);
}

.bl-map-note { margin-left: auto; }

.bl-map-none {
	padding: var(--spacing-lg, 18px);
	color: var(--text-secondary);
	font-size: var(--font-size-sm, 0.85rem);
}

@media (max-width: 640px) {
	.bl-map { height: 260px; }

	/* The foot is a row on a desktop and a stack on a phone. Left as a row it
	   pushes "Position from GPS" off the right edge, which is where the sentence
	   explaining the position ends up being invisible on the device most likely
	   to be asking where the boat is. */
	.bl-map-foot { align-items: flex-start; flex-direction: column; gap: 6px; }
	.bl-map-note { margin-left: 0; }

	/* Nothing in a card may make the page scroll sideways. */
	.bl-map-card { max-width: 100%; }
}

/* ── Choosing what a fleet card shows ────────────────────────────────────────
   The card itself is a link to the boat, so the picker lives beside it rather
   than inside it: a control within a link is one you cannot press. */
.bl-boat-cell {
	display: flex;
	flex-direction: column;
}

.bl-boat-cell > .bl-boat-card { flex: 1; }

.bl-metrics-none {
	color: var(--text-secondary);
	font-size: var(--font-size-sm, 0.82rem);
}

.bl-headline-toggle {
	width: 100%;
	margin-top: 6px;
	padding: 6px 10px;
	border: 1px dashed var(--border-color);
	border-radius: var(--radius-md, 8px);
	background: none;
	color: var(--text-secondary);
	font-size: var(--font-size-sm, 0.82rem);
	cursor: pointer;
}

.bl-headline-toggle:hover {
	color: var(--text-primary);
	border-style: solid;
	background: var(--bg-tertiary);
}

.bl-headline-panel {
	margin-top: 6px;
	padding: var(--spacing-sm, 10px);
	border: 1px solid var(--border-color);
	border-radius: var(--radius-md, 8px);
	background: var(--bg-secondary);
}

.bl-headline-panel[hidden] { display: none; }

.bl-headline-hint {
	margin: 0 0 8px;
	color: var(--text-secondary);
	font-size: var(--font-size-sm, 0.8rem);
}

/* Which sensor a number came from, under the number. The reason a reading is
   chosen per sensor and not per kind: two thermometers on one card are only
   useful if you can tell which is which. */
.bl-metric-from {
	display: block;
	margin-top: 2px;
	color: var(--text-secondary);
	font-size: 0.68rem;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

/* The picker lists every reading aboard, under the sensor that produces it. */
.bl-headline-group + .bl-headline-group {
	margin-top: 6px;
	padding-top: 6px;
	border-top: 1px solid var(--border-color);
}

.bl-headline-sensor {
	padding: 2px 8px;
	color: var(--text-primary);
	font-size: var(--font-size-sm, 0.78rem);
	font-weight: 600;
}

.bl-headline-where { color: var(--text-secondary); font-weight: 400; }

/* A boat can report a great many readings; the panel scrolls rather than
   running off the bottom of the card. */
.bl-headline-panel {
	max-height: 60vh;
	overflow-y: auto;
}

.bl-headline-choice {
	display: flex;
	align-items: center;
	gap: 8px;
	padding: 6px 8px;
	border-radius: var(--radius-sm, 6px);
	cursor: pointer;
}

.bl-headline-choice:hover { background: var(--bg-tertiary); }
.bl-headline-choice.is-on { background: var(--bg-tertiary); }
.bl-headline-name { flex: 1; font-size: var(--font-size-sm, 0.85rem); }

/* What that reading says right now, so the choice is made against real numbers
   rather than a list of words. */
.bl-headline-now {
	color: var(--text-secondary);
	font-size: var(--font-size-sm, 0.8rem);
	font-variant-numeric: tabular-nums;
}

/* A reading is shown however old it is - the last thing a sensor said beats a
   blank - but an old value looks exactly like a current one, and that is the
   one somebody would act on. */
.bl-sensor-stale {
	margin: 0 0 8px;
	padding: 6px 10px;
	border-radius: var(--radius-sm, 6px);
	background: rgba(245, 158, 11, 0.1);
	border: 1px solid rgba(245, 158, 11, 0.25);
	color: var(--text-secondary);
	font-size: var(--font-size-sm, 0.78rem);
	line-height: 1.45;
}

/* ── The alerts page: what happened, and what you get told about ──────────── */
.bl-alert-tabs {
	display: flex;
	gap: 4px;
	margin-bottom: var(--spacing-lg, 20px);
	border-bottom: 1px solid var(--border-color);
}

.bl-alert-tab {
	padding: 10px 14px;
	border: 0;
	border-bottom: 2px solid transparent;
	background: none;
	color: var(--text-secondary);
	font-size: var(--font-size-sm, 0.9rem);
	font-weight: 500;
	cursor: pointer;
}

.bl-alert-tab:hover { color: var(--text-primary); }

.bl-alert-tab.active {
	color: var(--text-primary);
	border-bottom-color: var(--accent, #10b981);
}

.bl-alert-boat + .bl-alert-boat { margin-top: var(--spacing-lg, 20px); }

.bl-alert-readonly {
	color: var(--text-secondary);
	font-size: var(--font-size-sm, 0.8rem);
}

.bl-alert-mode,
.bl-alert-none {
	margin: 0 0 var(--spacing-md, 14px);
	color: var(--text-secondary);
	font-size: var(--font-size-sm, 0.82rem);
	line-height: 1.5;
}

.bl-alert-device + .bl-alert-device {
	margin-top: var(--spacing-md, 14px);
	padding-top: var(--spacing-md, 14px);
	border-top: 1px solid var(--border-color);
}

.bl-alert-device-name { font-weight: 600; }
.bl-alert-where { color: var(--text-secondary); font-weight: 400; }

.bl-alert-row {
	display: flex;
	align-items: baseline;
	flex-wrap: wrap;
	gap: 8px;
	padding: 8px 0;
	cursor: pointer;
}

.bl-alert-row input[type="checkbox"] { cursor: pointer; }
.bl-alert-row input:disabled { cursor: default; }

.bl-alert-name { font-size: var(--font-size-sm, 0.9rem); }

.bl-alert-detail,
.bl-alert-now {
	color: var(--text-secondary);
	font-size: var(--font-size-sm, 0.8rem);
}

.bl-alert-now { margin-left: auto; font-variant-numeric: tabular-nums; }

/* Off a dashboard is not off an alarm. Marked, because somebody scanning this
   page has to know the row is still live. */
.bl-alert-hidden {
	margin-left: 6px;
	padding: 1px 6px;
	border-radius: 999px;
	background: var(--bg-tertiary);
	color: var(--text-secondary);
	font-size: 0.7rem;
}

.bl-alert-reading.is-off .bl-alert-name { color: var(--text-secondary); }

.bl-alert-limits {
	display: flex;
	flex-wrap: wrap;
	gap: var(--spacing-md, 14px);
	padding: 0 0 8px 26px;
}

.bl-alert-limit {
	display: flex;
	align-items: center;
	gap: 6px;
	font-size: var(--font-size-sm, 0.8rem);
	color: var(--text-secondary);
}

.bl-alert-limit input {
	width: 90px;
	padding: 5px 8px;
	border: 1px solid var(--border-color);
	border-radius: var(--radius-sm, 6px);
	background: var(--bg-secondary);
	color: var(--text-primary);
	font-variant-numeric: tabular-nums;
}

/* The duration, the code direction, and which state of an on/off reading is
   worth a message - all of them sat unstyled next to styled number boxes. */
.bl-alert-limit select {
	padding: 5px 8px;
	border: 1px solid var(--border-color);
	border-radius: var(--radius-sm, 6px);
	background: var(--bg-secondary);
	color: var(--text-primary);
	font-size: var(--font-size-sm, 0.8rem);
}

.bl-alert-unit { min-width: 2ch; }

/* ── Managing a boat's states ────────────────────────────────────────────── */
.bl-mode-manage {
	flex: none;
	padding: 4px 12px;
	border: 1px solid var(--border-color);
	border-radius: var(--radius-sm, 4px);
	background: transparent;
	color: var(--text-secondary);
	font-size: var(--font-size-sm, 0.8rem);
	cursor: pointer;
}

.bl-mode-manage:hover { border-color: var(--color-primary); color: var(--color-primary); }

.bl-mode-manager {
	margin: 0 0 var(--spacing-md, 14px);
	padding: var(--spacing-md, 14px);
	border: 1px solid var(--border-color);
	border-radius: var(--radius-md, 8px);
	background: var(--bg-secondary);
}

.bl-mode-manager-note {
	margin: 0 0 var(--spacing-sm, 10px);
	font-size: var(--font-size-sm, 0.8rem);
	color: var(--text-muted);
	line-height: 1.45;
}

.bl-mode-row {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--spacing-sm, 8px);
	padding: 7px 0;
	border-bottom: 1px solid var(--border-color);
}

.bl-mode-row:last-of-type { border-bottom: 0; }

.bl-mode-row-name { color: var(--text-primary); font-size: var(--font-size-sm, 0.88rem); }

.bl-mode-remove {
	flex: none;
	padding: 3px 10px;
	border: 1px solid var(--border-color);
	border-radius: var(--radius-sm, 4px);
	background: transparent;
	color: var(--text-muted);
	font-size: var(--font-size-sm, 0.78rem);
	cursor: pointer;
}

.bl-mode-remove:hover { border-color: var(--status-silent); color: var(--status-silent); }

.bl-mode-add {
	display: flex;
	gap: var(--spacing-sm, 8px);
	margin-top: var(--spacing-md, 12px);
	padding-top: var(--spacing-md, 12px);
	border-top: 1px solid var(--border-color);
}

.bl-mode-name { flex: 1; min-width: 0; }

/* ============ ADDING A DEVICE ============
   One button in the device area, and a modal list behind it. Kept quiet: adding
   a device is a deliberate act, not something the page nags about. */
.bl-add-device-row {
	display: flex;
	justify-content: center;
	margin: var(--spacing-lg, 20px) 0 var(--spacing-sm, 8px);
}

.bl-add-device {
	display: inline-flex;
	align-items: center;
	gap: var(--spacing-xs, 6px);
	padding: 9px 18px;
	border: 1px solid var(--color-primary);
	border-radius: var(--radius-md, 8px);
	background: transparent;
	color: var(--color-primary);
	font-size: var(--font-size-sm, 0.85rem);
	font-weight: 600;
	cursor: pointer;
	transition: background 0.12s ease, color 0.12s ease;
}

.bl-add-device:hover { background: var(--color-primary); color: #fff; }

.bl-add-device svg { width: 16px; height: 16px; }

/* The candidate list and its loading state live inside the modal now. */
.bl-detected-list { margin-top: var(--spacing-sm, 8px); }

.bl-modal-loading {
	display: flex;
	justify-content: center;
	padding: var(--spacing-xl, 28px);
}

.bl-detected-row {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--spacing-md, 12px);
	padding: var(--spacing-sm, 10px) 0;
	flex-wrap: wrap;
}

.bl-detected-row + .bl-detected-row {
	border-top: 1px solid var(--border-color);
}

.bl-detected-info {
	display: flex;
	flex-direction: column;
	gap: 2px;
	min-width: 0;
}

.bl-detected-mac {
	font-family: var(--font-mono, ui-monospace, monospace);
	font-size: var(--font-size-sm, 0.85rem);
	color: var(--text-primary);
}

.bl-detected-reports {
	font-size: var(--font-size-xs, 0.78rem);
	color: var(--text-muted);
}

.bl-detected-seen {
	font-size: var(--font-size-xs, 0.78rem);
	color: var(--text-muted);
	opacity: 0.85;
}

.bl-detected-actions {
	display: flex;
	gap: var(--spacing-sm, 8px);
	flex-shrink: 0;
}

.bl-detected-add,
.bl-adopt-submit {
	padding: 8px 16px;
	border: none;
	border-radius: var(--radius-md, 8px);
	background: var(--color-primary);
	color: #fff;
	font-size: var(--font-size-sm, 0.85rem);
	font-weight: 600;
	cursor: pointer;
	transition: opacity 0.12s ease;
}

.bl-detected-add:hover,
.bl-adopt-submit:hover { opacity: 0.9; }

.bl-detected-dismiss {
	padding: 8px 14px;
	border: 1px solid var(--border-color);
	border-radius: var(--radius-md, 8px);
	background: transparent;
	color: var(--text-secondary);
	font-size: var(--font-size-sm, 0.85rem);
	cursor: pointer;
	transition: border-color 0.12s ease, color 0.12s ease;
}

.bl-detected-dismiss:hover {
	border-color: var(--text-secondary);
	color: var(--text-primary);
}

/* The add form: labels stacked over their controls, the submit full width so it
   is an obvious end to the short flow. */
.bl-adopt-form { display: flex; flex-direction: column; }

.bl-adopt-label {
	margin: var(--spacing-md, 12px) 0 var(--spacing-xs, 4px);
	font-size: 0.75rem;
	text-transform: uppercase;
	letter-spacing: 0.05em;
	color: var(--text-muted);
}

.bl-adopt-label:first-child { margin-top: 0; }

.bl-adopt-actions {
	display: flex;
	gap: var(--spacing-sm, 8px);
	margin-top: var(--spacing-lg, 18px);
}

.bl-adopt-submit { flex: 1; }

.bl-adopt-back {
	padding: 8px 16px;
	border: 1px solid var(--border-color);
	border-radius: var(--radius-md, 8px);
	background: transparent;
	color: var(--text-secondary);
	font-size: var(--font-size-sm, 0.85rem);
	cursor: pointer;
}

.bl-adopt-back:hover { color: var(--text-primary); border-color: var(--text-secondary); }

/* ── The boat dashboard ──────────────────────────────────────────────────────
   A cockpit console: one dark field, a strip of standing facts across the top,
   systems down the left, the vessel in the middle, the numbers along the foot.
   Its own colour scope rather than the app's cards, because a console is a
   different object from a page - it should read as a screen set into a helm.  */

.bl-dash {
	--con-ink: #e8edf2;
	--con-dim: #8b98a4;
	--con-faint: #5a6672;
	--con-line: rgba(255, 255, 255, .07);
	--con-accent: #5b9dd9;
	--con-warn: #e0a33d;
	--con-crit: #ef6b60;
}

.bl-console {
	position: relative;
	display: grid;
	grid-template-rows: auto 1fr auto auto;
	gap: 0;
	min-height: min(76vh, 720px);
	border-radius: 16px;
	overflow: hidden;
	color: var(--con-ink);
	background:
		radial-gradient(120% 90% at 50% -10%, #1b2733 0%, #0d141b 45%, #080c11 100%);
	box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .05), 0 18px 50px rgba(0, 0, 0, .45);
	font-variant-numeric: tabular-nums;
}

.bl-dash-empty { padding: 60px 20px; text-align: center; color: var(--text-muted); }

/* ── top strip ── */
.bl-console-strip {
	display: grid;
	grid-template-columns: repeat(4, auto);
	justify-content: space-between;
	gap: 10px 28px;
	padding: 16px clamp(16px, 3vw, 30px) 14px;
	border-bottom: 1px solid var(--con-line);
}
.bl-strip-group { display: grid; gap: 3px; min-width: 0; }
.bl-strip-group.right { justify-items: end; text-align: right; }
.bl-strip-line {
	font: 500 11.5px/1.35 ui-monospace, "SF Mono", Menlo, Consolas, monospace;
	letter-spacing: .09em;
	white-space: nowrap;
	color: var(--con-ink);
}
.bl-strip-line.dim { color: var(--con-faint); }

/* ── body: rail + stage ── */
.bl-console-body {
	display: grid;
	grid-template-columns: 104px 1fr;
	min-height: 0;
}

.bl-console-rail {
	display: flex;
	flex-direction: column;
	gap: 4px;
	padding: 20px 0;
	border-right: 1px solid var(--con-line);
}
.bl-rail-item {
	display: grid;
	justify-items: center;
	gap: 7px;
	padding: 13px 4px;
	background: none;
	border: 0;
	cursor: pointer;
	color: var(--con-faint);
	transition: color .15s;
}
.bl-rail-item:hover { color: var(--con-dim); }
.bl-rail-item.on { color: var(--con-accent); }
.bl-rail-icon { position: relative; display: block; }
.bl-rail-icon svg { width: 21px; height: 21px; display: block; }
.bl-rail-lamp {
	position: absolute; top: -3px; right: -8px;
	width: 7px; height: 7px; border-radius: 50%;
	background: var(--con-warn);
	box-shadow: 0 0 8px currentColor;
}
.bl-rail-lamp.crit { background: var(--con-crit); animation: conPulse 1.8s ease-in-out infinite; }
@keyframes conPulse { 50% { opacity: .35; } }
.bl-rail-label {
	font: 600 9.5px/1 ui-monospace, "SF Mono", Menlo, Consolas, monospace;
	letter-spacing: .14em;
	text-transform: uppercase;
}

/* ── stage ── */
.bl-console-stage {
	position: relative;
	display: grid;
	place-items: center;
	padding: 18px clamp(14px, 3vw, 34px) 6px;
	min-height: 0;
}

.bl-console-callout {
	position: absolute;
	top: 14px; left: clamp(14px, 3vw, 34px);
	display: grid;
	gap: 5px;
	max-width: min(52%, 420px);
	pointer-events: none;
}
.bl-callout-rule { width: 96px; height: 2px; background: var(--con-crit); border-radius: 2px; }
.bl-console-callout.warn .bl-callout-rule { background: var(--con-warn); }
.bl-console-callout.ok .bl-callout-rule { background: var(--con-accent); }
.bl-callout-kicker {
	font: 600 9.5px/1 ui-monospace, "SF Mono", Menlo, Consolas, monospace;
	letter-spacing: .18em;
	color: var(--con-faint);
}
.bl-callout-text {
	font-size: 15.5px;
	line-height: 1.3;
	letter-spacing: .01em;
	color: var(--con-ink);
	text-transform: uppercase;
}
.bl-callout-detail { font-size: 12.5px; line-height: 1.45; color: var(--con-dim); text-transform: none; }
/* The leader: down from the text, then along under the hull. */
.bl-callout-leader {
	margin-top: 6px;
	width: 150px; height: 132px;
	border-left: 1px solid var(--con-line);
	border-bottom: 1px solid var(--con-line);
}

.bl-console-anchor {
	position: absolute;
	top: 20px; right: clamp(14px, 3vw, 34px);
	display: flex; align-items: center; gap: 9px;
	color: var(--con-dim);
}
.bl-console-anchor.crit { color: var(--con-crit); }
.bl-anchor-label {
	font: 600 10px/1 ui-monospace, "SF Mono", Menlo, Consolas, monospace;
	letter-spacing: .18em;
}
.bl-anchor-icon svg { width: 19px; height: 19px; display: block; }

/* ── metrics ── */
.bl-console-metrics {
	display: flex;
	align-items: flex-end;
	gap: clamp(18px, 3vw, 42px);
	padding: 14px clamp(16px, 3vw, 30px) 16px;
	border-top: 1px solid var(--con-line);
	overflow-x: auto;
	scrollbar-width: none;
}
.bl-console-metrics::-webkit-scrollbar { display: none; }
.bl-metric-group { display: grid; gap: 8px; flex: none; }
.bl-metric-group-name {
	font: 600 9.5px/1 ui-monospace, "SF Mono", Menlo, Consolas, monospace;
	letter-spacing: .18em;
	text-transform: uppercase;
	color: var(--con-faint);
}
.bl-metric-row { display: flex; gap: clamp(14px, 2.4vw, 34px); }
.bl-metric { display: grid; gap: 3px; }
.bl-metric-label {
	font-size: 12px;
	letter-spacing: .04em;
	text-transform: uppercase;
	color: var(--con-dim);
	white-space: nowrap;
}
.bl-metric-value {
	font: 300 clamp(26px, 3.4vw, 38px)/1 var(--font-display, inherit);
	letter-spacing: -.015em;
	white-space: nowrap;
}
.bl-metric-unit {
	font-size: 12.5px;
	font-weight: 500;
	letter-spacing: .06em;
	color: var(--con-dim);
	margin-left: 4px;
}
.bl-metric-none { color: var(--con-faint); font-size: 13px; }

/* ── foot ── */
.bl-console-foot {
	display: flex;
	align-items: center;
	gap: 12px;
	padding: 11px clamp(16px, 3vw, 30px) 13px;
	border-top: 1px solid var(--con-line);
}
.bl-foot-gear {
	display: grid; place-items: center;
	width: 26px; height: 26px;
	border-radius: 7px;
	background: rgba(91, 157, 217, .16);
	color: var(--con-accent);
	font: 600 13px/1 ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}
.bl-foot-mode { font-size: 13px; color: var(--con-dim); }
.bl-foot-spacer { flex: 1; }
.bl-foot-link {
	font: 600 10px/1 ui-monospace, "SF Mono", Menlo, Consolas, monospace;
	letter-spacing: .16em;
	text-transform: uppercase;
	color: var(--con-faint);
}

/* ── phones: the rail lies down, the callout stops overlapping the boat ── */
@media (max-width: 720px) {
	.bl-console { min-height: 0; }
	.bl-console-strip { grid-template-columns: 1fr 1fr; gap: 10px 16px; }
	.bl-strip-group.right { justify-items: start; text-align: left; }
	.bl-console-body { grid-template-columns: 1fr; }
	.bl-console-rail {
		flex-direction: row;
		justify-content: space-around;
		padding: 10px 4px;
		border-right: 0;
		border-bottom: 1px solid var(--con-line);
		overflow-x: auto;
		scrollbar-width: none;
	}
	.bl-console-rail::-webkit-scrollbar { display: none; }
	.bl-rail-item { flex: none; padding: 6px 10px; }
	.bl-console-callout { position: static; max-width: none; margin-bottom: 4px; }
	.bl-callout-leader { display: none; }
	.bl-console-stage { grid-template-rows: auto auto; padding-bottom: 14px; }
	.bl-console-anchor { top: auto; bottom: 8px; right: 12px; }
	.bl-metric-value { font-size: 27px; }
}

/* ── the vessel, and the lamps laid over her ─────────────────────────────────
   The boat is a picture and the lamps are positioned in percentages of it, so
   swapping the drawing for a photograph or a real render moves nothing.      */

.bl-vessel-wrap {
	position: relative;
	width: min(100%, 760px);
	margin: 0 auto;
	line-height: 0;
}
.bl-vessel-img {
	width: 100%;
	height: auto;
	display: block;
	filter: drop-shadow(0 22px 30px rgba(0, 0, 0, .55));
	user-select: none;
	-webkit-user-drag: none;
}

.bl-lamp {
	position: absolute;
	transform: translate(-50%, -50%);
	display: grid;
	place-items: center;
	width: 22px; height: 22px;
	padding: 0;
	background: none;
	border: 0;
	cursor: pointer;
	line-height: 1;
	color: #7fd8ff;
}
.bl-lamp.warn { color: var(--con-warn, #e0a33d); }
.bl-lamp.crit { color: var(--con-crit, #ef6b60); }

.bl-lamp-core {
	width: 9px; height: 9px;
	border-radius: 50%;
	background: currentColor;
	box-shadow: 0 0 0 4px color-mix(in srgb, currentColor 22%, transparent),
	            0 0 16px currentColor;
}
.bl-lamp.crit .bl-lamp-core { animation: conPulse 1.6s ease-in-out infinite; }

/* The label sits beside the lamp and keeps out of the way until wanted. */
.bl-lamp-flag {
	position: absolute;
	left: 18px;
	display: grid;
	gap: 1px;
	padding: 4px 9px;
	border-radius: 8px;
	background: rgba(8, 12, 16, .82);
	backdrop-filter: blur(6px);
	border: 1px solid rgba(255, 255, 255, .08);
	white-space: nowrap;
	opacity: .9;
	transition: opacity .15s, transform .15s;
	pointer-events: none;
}
.bl-lamp:hover .bl-lamp-flag, .bl-lamp:focus-visible .bl-lamp-flag { opacity: 1; transform: translateX(2px); }
.bl-lamp-name {
	font: 600 8.5px/1 ui-monospace, "SF Mono", Menlo, Consolas, monospace;
	letter-spacing: .16em;
	color: var(--con-faint, #5a6672);
}
.bl-lamp-value {
	font-size: 12.5px;
	font-weight: 600;
	color: var(--con-ink, #e8edf2);
}

/* Only while the page is being arranged. */
.bl-vessel-wrap.editable .bl-lamp { cursor: grab; }
.bl-vessel-wrap.editable .bl-lamp::after {
	content: '';
	position: absolute;
	inset: -9px;
	border: 1px dashed color-mix(in srgb, currentColor 55%, transparent);
	border-radius: 50%;
}
.bl-vessel-wrap.editable .bl-lamp.dragging { cursor: grabbing; }
.bl-vessel-wrap.editable .bl-lamp.dragging .bl-lamp-core { box-shadow: 0 0 0 7px color-mix(in srgb, currentColor 28%, transparent); }

/* ── an alert you can put away for now ── */
.bl-alert-dismiss {
	flex: none;
	align-self: flex-start;
	width: 26px; height: 26px;
	margin: -2px -4px 0 4px;
	border: 0;
	border-radius: 7px;
	background: none;
	color: var(--text-muted);
	font-size: 19px;
	line-height: 1;
	cursor: pointer;
}
.bl-alert-dismiss:hover { background: rgba(255, 255, 255, .07); color: var(--text-primary); }

/* ── arranging the dashboard ─────────────────────────────────────────────────
   Only up while the page is in edit mode. Deliberately plain: it is a list of
   what the boat reports, with two decisions per row.                          */

.bl-dash-editor {
	margin-top: var(--spacing-lg, 16px);
	background: var(--bg-card);
	border: 1px solid var(--border-color);
	border-radius: 12px;
	padding: 16px 18px 18px;
}
.bl-editor-head {
	display: grid;
	gap: 4px;
	padding-bottom: 12px;
	margin-bottom: 12px;
	border-bottom: 1px solid var(--border-color);
}
.bl-editor-title { font-size: 15px; font-weight: 650; }
.bl-editor-hint { font-size: 13px; color: var(--text-secondary); line-height: 1.5; }
.bl-editor-reset {
	justify-self: start;
	margin-top: 6px;
	padding: 5px 12px;
	border: 1px solid var(--border-color);
	border-radius: 8px;
	background: none;
	color: var(--text-secondary);
	font-size: 12.5px;
	cursor: pointer;
}
.bl-editor-reset:hover { color: var(--text-primary); border-color: var(--color-primary); }

.bl-editor-list {
	display: grid;
	gap: 14px;
	grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}
.bl-editor-group { display: grid; gap: 3px; align-content: start; }
.bl-editor-device {
	font: 600 10px/1 ui-monospace, "SF Mono", Menlo, Consolas, monospace;
	letter-spacing: .14em;
	text-transform: uppercase;
	color: var(--text-muted);
	margin-bottom: 3px;
}
.bl-editor-row { display: flex; align-items: center; gap: 8px; }
.bl-editor-pick {
	flex: 1;
	display: flex; align-items: center; gap: 9px;
	padding: 6px 8px;
	border-radius: 8px;
	cursor: pointer;
	min-width: 0;
}
.bl-editor-pick:hover { background: var(--bg-tertiary); }
.bl-editor-pick input { accent-color: var(--color-primary); flex: none; }
.bl-editor-name { flex: 1; font-size: 13.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bl-editor-now {
	font: 500 12px ui-monospace, "SF Mono", Menlo, Consolas, monospace;
	color: var(--text-muted);
	white-space: nowrap;
}
.bl-editor-lamp {
	flex: none;
	padding: 5px 10px;
	border: 1px solid var(--border-color);
	border-radius: 999px;
	background: none;
	color: var(--text-muted);
	font: 600 10.5px ui-monospace, "SF Mono", Menlo, Consolas, monospace;
	letter-spacing: .08em;
	cursor: pointer;
}
.bl-editor-lamp:hover { color: var(--text-primary); border-color: var(--color-primary); }
.bl-editor-lamp.on {
	color: #7fd8ff;
	border-color: rgba(127, 216, 255, .5);
	background: rgba(127, 216, 255, .1);
}

/* A lamp whose reading has stopped arriving: still there, plainly empty. */
.bl-lamp.dark { color: var(--con-faint, #5a6672); }
.bl-lamp.dark .bl-lamp-core {
	background: none;
	border: 2px solid currentColor;
	width: 11px; height: 11px;
	box-shadow: none;
}
.bl-lamp.dark .bl-lamp-value { color: var(--con-faint, #5a6672); }

/* ── the console, full screen ── */
.bl-console-fs {
	position: absolute;
	right: clamp(14px, 3vw, 34px); bottom: 12px;
	z-index: 20;
	display: grid; place-items: center;
	width: 34px; height: 34px;
	border: 1px solid var(--con-line, rgba(255,255,255,.07));
	border-radius: 9px;
	background: rgba(10, 14, 18, .7);
	backdrop-filter: blur(6px);
	color: var(--con-dim, #8b98a4);
	cursor: pointer;
}
.bl-console-fs svg { width: 16px; height: 16px; }
.bl-console-fs:hover { color: var(--con-ink, #e8edf2); border-color: rgba(255, 255, 255, .18); }

.bl-console:fullscreen {
	border-radius: 0;
	min-height: 100vh;
}
.bl-console:fullscreen .bl-vessel-wrap { width: min(92%, 1080px); }

/* ── the live vessel ─────────────────────────────────────────────────────────
   The photograph is the layout box and the fallback; when WebGL is willing,
   the live render sits over it in the same frame, so the lamp percentages
   stay true from the parked pose. While the camera is away visiting a part,
   the lamps hide - their positions are only honest from the parked pose.   */

.bl-vessel-wrap { aspect-ratio: 1500 / 620; }
.bl-vessel-3d { position: absolute; inset: 0; }
.bl-vessel-3d canvas { width: 100% !important; height: 100% !important; display: block; }
.bl-vessel-wrap.live .bl-vessel-img { visibility: hidden; }
.bl-vessel-wrap .bl-lamp { transition: opacity .25s; }
.bl-vessel-wrap.focused .bl-lamp { opacity: 0; pointer-events: none; }

/* ---- Fancy speaks the demo's type ---------------------------------------
   Georgia for names and numerals - the same face as the BL monogram - and
   the platform mono stays where data already wears it. Prose keeps the
   system face, exactly the division the demo page draws. */
:root[data-bl-theme="fancy"] {
	--bl-serif: Georgia, 'Iowan Old Style', 'Times New Roman', serif;
}

:root[data-bl-theme="fancy"] h1,
:root[data-bl-theme="fancy"] h2,
:root[data-bl-theme="fancy"] h3,
:root[data-bl-theme="fancy"] .bl-logo,
:root[data-bl-theme="fancy"] .bl-boat-name,
:root[data-bl-theme="fancy"] .bl-metric-value,
:root[data-bl-theme="fancy"] .bl-value,
:root[data-bl-theme="fancy"] .bl-pop-depth {
	font-family: var(--bl-serif);
}

/* ---- the wardrobe under the embedded dashboard --------------------------
   The demo's editor, restyled in the app's own tokens so it follows both
   themes. Same classes as the demo, deliberately: one vocabulary. */
.d2c-host { margin-top: 14px; padding: 18px; }
.d2c-lead { margin: 2px 0 14px; color: var(--text-secondary); font-size: 0.85rem; max-width: 72ch; }

.d2c-grid {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
	gap: 18px;
}
.d2c-block { display: grid; gap: 8px; align-content: start; }
.d2c-block h4 { margin: 0 0 2px; font-size: 0.85rem; color: var(--text-primary); }
.d2c-block h4 small { display: block; font-weight: 500; font-size: 0.72rem; color: var(--text-muted); }
.d2c-none { margin: 0; color: var(--text-muted); font-size: 0.82rem; }

.d2c-slot {
	display: grid;
	grid-template-columns: 52px minmax(0, 1fr);
	align-items: start;
	gap: 6px 10px;
	padding: 8px 10px;
	border: 1px solid var(--border-color);
	border-radius: 10px;
	background: var(--bg-primary);
	margin-bottom: 6px;
}
.d2c-slot-n {
	font: 600 9.5px/2.6 ui-monospace, monospace;
	letter-spacing: 0.14em;
	text-transform: uppercase;
	color: var(--text-muted);
	white-space: nowrap;
}
.d2c-slot select, .d2c-slot .d2c-rename {
	width: 100%;
	background: var(--bg-card);
	color: var(--text-primary);
	border: 1px solid var(--border-color);
	border-radius: 8px;
	font-size: 0.8rem;
	padding: 6px 8px;
}
.d2c-rename:focus { border-color: var(--border-focus); outline: none; }
.d2c-slot-more {
	grid-column: 2;
	display: grid;
	grid-template-columns: minmax(0, 1fr) auto;
	gap: 6px;
}
.d2c-slot-more select { max-width: 165px; }

.d2c-vessels { display: grid; gap: 6px; }
.d2c-vessel {
	display: flex; align-items: center; gap: 9px;
	padding: 9px 11px;
	border: 1px solid var(--border-color);
	border-radius: 10px;
	background: var(--bg-primary);
	font-size: 0.82rem;
	cursor: pointer;
}
.d2c-vessel:has(input:checked) { border-color: var(--border-focus); background: var(--bg-tertiary); }
.d2c-vessel input { accent-color: var(--color-primary); }

.d2c-wide { grid-column: 1 / -1; }
.d2c-cardgroup { display: grid; gap: 8px; margin-bottom: 14px; }
.d2c-grouplabel {
	font: 600 9.5px/1 ui-monospace, monospace;
	letter-spacing: 0.18em;
	text-transform: uppercase;
	color: var(--text-muted);
}
.d2c-cardgrid {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
	gap: 8px;
}
.d2c-card {
	display: grid;
	gap: 3px;
	text-align: left;
	padding: 10px 12px;
	border: 1px solid var(--border-color);
	border-radius: 10px;
	background: var(--bg-primary);
	color: inherit;
	cursor: pointer;
	transition: border-color 0.15s ease, opacity 0.15s ease;
}
.d2c-card small {
	font: 600 9px/1.2 ui-monospace, monospace;
	letter-spacing: 0.14em;
	text-transform: uppercase;
	color: var(--text-muted);
	overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.d2c-card strong {
	font: 400 20px/1.1 Georgia, 'Iowan Old Style', serif;
	color: var(--text-primary);
	white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.d2c-card span {
	font-size: 10.5px;
	color: var(--text-secondary);
	min-height: 13px;
	overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.d2c-card.on { border-color: var(--border-focus); background: var(--bg-tertiary); }
.d2c-card.on small { color: var(--color-primary); }
.d2c-card.stale-a strong { color: var(--status-late); }
.d2c-card.stale-r strong { color: var(--status-silent); }
.d2c-card:not(.on) { opacity: 0.62; }
.d2c-card:not(.on):hover { opacity: 1; border-color: var(--border-focus); }

.d2c-foot { display: flex; align-items: center; gap: 14px; margin-top: 14px; }
.d2c-status { color: var(--status-online); font-size: 0.8rem; letter-spacing: 0.08em; }

/* The systems picker: which cells this boat carries. */
.d2c-syslist { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
.d2c-sys {
	display: flex; align-items: center; gap: 8px;
	padding: 7px 10px;
	border: 1px solid var(--border-color);
	border-radius: 9px;
	background: var(--bg-primary);
	font-size: 12.5px;
	cursor: pointer;
}
.d2c-sys:has(input:checked) { border-color: var(--border-focus); background: var(--bg-tertiary); }
.d2c-sys input { accent-color: var(--color-primary); }
