/* =============================================================================
   SafetyStop web app styles.

   STARTED as a verbatim extraction of www/index.html's <style> block, which is still a byte-for-byte
   substring of the region above the "web-only additions" marker — but that region is no longer a pure copy,
   and RE-EXTRACTING IT WOULD DELETE the desktop rules appended above the marker. Change the app's look in
   www/index.html first and port it here. Nothing sha256-pins this file — read the marker before editing it.
   ============================================================================= */
    :root {
      --bg: #0b0f19;
      --card-bg: rgba(17, 24, 39, 0.75);
      --border: rgba(255, 255, 255, 0.1);
      --text: #f3f4f6;
      --text-muted: #9ca3af;
      --primary: #00f2fe;
      --primary-glow: rgba(0, 242, 254, 0.25);
      --accent: #4facfe;
      --safe: #10b981;
      --warning: #f59e0b;
      --danger: #ef4444;
    }

    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
      -webkit-tap-highlight-color: transparent;
    }

    body {
      background-color: var(--bg);
      color: var(--text);
      min-height: 100vh;
      background-image:
        radial-gradient(circle at 15% 15%, rgba(0, 242, 254, 0.08) 0%, transparent 40%),
        radial-gradient(circle at 85% 85%, rgba(79, 172, 254, 0.08) 0%, transparent 40%);
    }

    /* --- App shell: scrollable content + fixed bottom tab bar --- */
    .app-shell {
      display: flex;
      flex-direction: column;
      min-height: 100vh;
    }

    .app-content {
      flex: 1;
      overflow-y: auto;
      -webkit-overflow-scrolling: touch;
      padding: max(16px, env(safe-area-inset-top)) max(16px, env(safe-area-inset-right))
               calc(72px + env(safe-area-inset-bottom)) max(16px, env(safe-area-inset-left));
      display: flex;
      justify-content: center;
    }

    .content-inner {
      width: 100%;
      max-width: 500px;
    }

    .calculator-card {
      background: var(--card-bg);
      backdrop-filter: blur(16px);
      -webkit-backdrop-filter: blur(16px);
      border: 1px solid var(--border);
      border-radius: 24px;
      padding: 24px;
      width: 100%;
      box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
    }

    /* --- SafetyStop brand header --- */
    .brand-header {
      text-align: center;
      margin-bottom: 20px;
    }

    .brand-header h1 {
      font-size: 1.9rem;
      font-weight: 800;
      letter-spacing: 0.02em;
      background: linear-gradient(135deg, var(--primary), var(--accent));
      -webkit-background-clip: text;
      background-clip: text;
      -webkit-text-fill-color: transparent;
    }

    .brand-header .tagline {
      color: var(--text-muted);
      font-size: 0.8rem;
      font-style: italic;
      margin-top: 2px;
    }

    /* --- Bottom tab bar --- */
    .bottom-nav {
      position: fixed;
      bottom: 0;
      left: 0;
      right: 0;
      display: flex;
      background: rgba(11, 15, 25, 0.85);
      backdrop-filter: blur(20px);
      -webkit-backdrop-filter: blur(20px);
      border-top: 1px solid var(--border);
      padding-bottom: env(safe-area-inset-bottom);
      z-index: 50;
    }

    .nav-btn {
      flex: 1;
      background: none;
      border: none;
      padding: 10px 4px;
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 3px;
      color: var(--text-muted);
      font-size: 0.65rem;
      font-weight: 600;
      cursor: pointer;
      transition: color 0.2s ease;
    }

    .nav-btn .nav-icon {
      font-size: 1.3rem;
      line-height: 1;
    }

    .nav-btn.active {
      color: var(--primary);
    }

    .header {
      text-align: center;
      margin-bottom: 24px;
    }

    .header h1 {
      font-size: 1.6rem;
      font-weight: 700;
      background: linear-gradient(135deg, var(--primary), var(--accent));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      margin-bottom: 4px;
    }

    .header p {
      color: var(--text-muted);
      font-size: 0.85rem;
    }

    .unit-toggle {
      display: flex;
      background: rgba(0, 0, 0, 0.3);
      border-radius: 12px;
      padding: 4px;
      border: 1px solid var(--border);
      margin-bottom: 20px;
    }

    .unit-btn {
      flex: 1;
      padding: 10px;
      border: none;
      background: transparent;
      color: var(--text-muted);
      font-size: 0.85rem;
      font-weight: 600;
      border-radius: 8px;
      cursor: pointer;
      transition: all 0.2s ease;
    }

    .unit-btn.active {
      background: linear-gradient(135deg, var(--primary), var(--accent));
      color: #000;
      box-shadow: 0 2px 8px var(--primary-glow);
    }

    .input-group {
      margin-bottom: 20px;
    }

    .input-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 8px;
    }

    .input-label {
      font-size: 0.85rem;
      font-weight: 600;
      color: var(--text);
    }

    .input-value {
      font-size: 1rem;
      font-weight: 700;
      color: var(--primary);
    }

    input[type="range"] {
      width: 100%;
      height: 8px;
      border-radius: 4px;
      background: rgba(255, 255, 255, 0.1);
      outline: none;
      -webkit-appearance: none;
    }

    input[type="range"]::-webkit-slider-thumb {
      -webkit-appearance: none;
      width: 24px;
      height: 24px;
      border-radius: 50%;
      background: var(--primary);
      cursor: pointer;
      box-shadow: 0 0 10px var(--primary-glow);
    }

    .preset-row {
      display: flex;
      gap: 6px;
      margin-top: 10px;
    }

    .preset-btn {
      flex: 1;
      padding: 8px 4px;
      background: rgba(255, 255, 255, 0.03);
      border: 1px solid var(--border);
      border-radius: 8px;
      color: var(--text-muted);
      font-size: 0.75rem;
      font-weight: 600;
      cursor: pointer;
      transition: all 0.2s ease;
    }

    .preset-btn:hover {
      color: var(--text);
      border-color: var(--primary);
    }

    .preset-btn.active {
      background: linear-gradient(135deg, var(--primary), var(--accent));
      color: #000;
      border-color: transparent;
      box-shadow: 0 2px 8px var(--primary-glow);
    }

    .status-badge {
      display: flex;
      align-items: center;
      gap: 10px;
      padding: 12px 16px;
      border-radius: 12px;
      font-size: 0.85rem;
      font-weight: 600;
      margin-bottom: 20px;
      transition: all 0.3s ease;
    }

    .status-badge.safe {
      background: rgba(16, 185, 129, 0.15);
      border: 1px solid rgba(16, 185, 129, 0.3);
      color: var(--safe);
    }

    .status-badge.warning {
      background: rgba(245, 158, 11, 0.15);
      border: 1px solid rgba(245, 158, 11, 0.3);
      color: var(--warning);
    }

    .status-badge.danger {
      background: rgba(239, 68, 68, 0.15);
      border: 1px solid rgba(239, 68, 68, 0.3);
      color: var(--danger);
    }

    .results-grid {
      display: grid;
      grid-template-columns: repeat(2, 1fr);
      gap: 12px;
    }

    .result-card {
      background: rgba(255, 255, 255, 0.03);
      border: 1px solid var(--border);
      border-radius: 16px;
      padding: 14px;
      text-align: center;
    }

    .result-title {
      font-size: 0.7rem;
      text-transform: uppercase;
      letter-spacing: 0.05em;
      color: var(--text-muted);
      margin-bottom: 4px;
    }

    .result-val {
      font-size: 1.4rem;
      font-weight: 700;
      color: var(--text);
    }

    .result-unit {
      font-size: 0.75rem;
      color: var(--text-muted);
      font-weight: normal;
    }

    .key-trigger {
      width: 100%;
      margin-top: 16px;
      padding: 12px;
      background: rgba(255, 255, 255, 0.03);
      border: 1px solid var(--border);
      border-radius: 12px;
      color: var(--text-muted);
      font-size: 0.8rem;
      font-weight: 600;
      cursor: pointer;
      transition: all 0.2s ease;
    }

    .key-trigger:hover {
      color: var(--text);
      border-color: var(--primary);
    }

    .modal-overlay {
      position: fixed;
      inset: 0;
      background: rgba(0, 0, 0, 0.6);
      backdrop-filter: blur(4px);
      -webkit-backdrop-filter: blur(4px);
      display: none;
      justify-content: center;
      align-items: center;
      padding: 20px;
      z-index: 100;
    }

    .modal-overlay.open {
      display: flex;
    }

    .modal {
      background: var(--card-bg);
      backdrop-filter: blur(16px);
      -webkit-backdrop-filter: blur(16px);
      border: 1px solid var(--border);
      border-radius: 20px;
      padding: 24px;
      width: 100%;
      max-width: 420px;
      box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5);
      max-height: 85vh;
      overflow-y: auto;
    }

    .modal-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 14px;
    }

    .modal-close {
      background: transparent;
      border: none;
      color: var(--text-muted);
      font-size: 1.6rem;
      line-height: 1;
      cursor: pointer;
      padding: 0 4px;
    }

    .modal-close:hover {
      color: var(--text);
    }

    .key-title {
      font-size: 0.7rem;
      text-transform: uppercase;
      letter-spacing: 0.05em;
      color: var(--text-muted);
      margin-bottom: 10px;
    }

    .key-list {
      display: grid;
      grid-template-columns: auto 1fr;
      column-gap: 12px;
      row-gap: 6px;
      font-size: 0.8rem;
    }

    .key-list dt {
      font-weight: 700;
      color: var(--primary);
      white-space: nowrap;
    }

    .key-list dd {
      color: var(--text-muted);
      margin: 0;
    }

    /* --- Tabs --- */
    .tab-bar {
      display: flex;
      background: rgba(0, 0, 0, 0.3);
      border-radius: 12px;
      padding: 4px;
      border: 1px solid var(--border);
      margin-bottom: 24px;
    }

    .tab-btn {
      flex: 1;
      padding: 10px;
      border: none;
      background: transparent;
      color: var(--text-muted);
      font-size: 0.9rem;
      font-weight: 600;
      border-radius: 8px;
      cursor: pointer;
      transition: all 0.2s ease;
    }

    .tab-btn.active {
      background: linear-gradient(135deg, var(--primary), var(--accent));
      color: #000;
      box-shadow: 0 2px 8px var(--primary-glow);
    }

    .tab-panel {
      display: none;
    }

    .tab-panel.active {
      display: block;
    }

    /* --- Logbook --- */
    .logbook-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 16px;
    }

    .logbook-header h2 {
      font-size: 1.1rem;
      font-weight: 700;
    }

    .btn-primary {
      padding: 10px 16px;
      border: none;
      border-radius: 10px;
      background: linear-gradient(135deg, var(--primary), var(--accent));
      color: #000;
      font-size: 0.85rem;
      font-weight: 700;
      cursor: pointer;
      box-shadow: 0 2px 8px var(--primary-glow);
    }

    .empty-state {
      text-align: center;
      padding: 40px 20px;
      color: var(--text-muted);
      font-size: 0.9rem;
    }

    .dive-list {
      display: flex;
      flex-direction: column;
      gap: 12px;
    }

    .dive-card {
      background: rgba(255, 255, 255, 0.03);
      border: 1px solid var(--border);
      border-radius: 16px;
      padding: 16px;
    }

    .dive-card-top {
      display: flex;
      justify-content: space-between;
      align-items: baseline;
      margin-bottom: 4px;
    }

    .dive-site {
      font-size: 1rem;
      font-weight: 700;
      color: var(--text);
    }

    .dive-date {
      font-size: 0.75rem;
      color: var(--text-muted);
    }

    .dive-stats {
      display: flex;
      flex-wrap: wrap;
      gap: 8px 16px;
      margin: 10px 0;
      font-size: 0.8rem;
      color: var(--text-muted);
    }

    .dive-stats b {
      color: var(--text);
      font-weight: 700;
    }

    .dive-calc {
      display: flex;
      flex-wrap: wrap;
      gap: 6px;
      margin-top: 10px;
      padding-top: 10px;
      border-top: 1px solid var(--border);
    }

    .calc-pill {
      font-size: 0.72rem;
      font-weight: 600;
      padding: 4px 8px;
      border-radius: 6px;
      background: rgba(0, 242, 254, 0.1);
      color: var(--primary);
      border: 1px solid rgba(0, 242, 254, 0.2);
    }

    .calc-pill.warn { background: rgba(245, 158, 11, 0.12); color: var(--warning); border-color: rgba(245, 158, 11, 0.25); }
    .calc-pill.bad  { background: rgba(239, 68, 68, 0.12); color: var(--danger); border-color: rgba(239, 68, 68, 0.25); }

    .dive-notes {
      margin-top: 10px;
      font-size: 0.8rem;
      color: var(--text-muted);
      white-space: pre-wrap;
    }

    .dive-actions {
      display: flex;
      gap: 8px;
      margin-top: 12px;
    }

    .btn-small {
      padding: 6px 12px;
      border-radius: 8px;
      border: 1px solid var(--border);
      background: transparent;
      color: var(--text-muted);
      font-size: 0.75rem;
      font-weight: 600;
      cursor: pointer;
      transition: all 0.2s ease;
    }

    .btn-small:hover { color: var(--text); border-color: var(--primary); }
    .btn-small.danger:hover { color: var(--danger); border-color: var(--danger); }

    /* --- Form --- */
    .dive-form {
      display: flex;
      flex-direction: column;
      gap: 14px;
    }

    .form-row {
      display: flex;
      gap: 12px;
    }

    .form-field {
      flex: 1 1 0;
      /* min-width:0 lets the field shrink below its content's intrinsic width.
         Without it, wide native controls (e.g. <input type="date"> on iOS)
         refuse to shrink and overflow into the neighbouring field. */
      min-width: 0;
      display: flex;
      flex-direction: column;
      gap: 6px;
    }

    .form-field label {
      font-size: 0.78rem;
      font-weight: 600;
      color: var(--text-muted);
    }

    .form-field input,
    .form-field select,
    .form-field textarea {
      width: 100%;
      padding: 10px 12px;
      background: rgba(0, 0, 0, 0.3);
      border: 1px solid var(--border);
      border-radius: 10px;
      color: var(--text);
      font-size: 0.9rem;
    }

    .form-field input:focus,
    .form-field select:focus,
    .form-field textarea:focus {
      outline: none;
      border-color: var(--primary);
    }

    .form-field textarea { resize: vertical; min-height: 60px; }

    /* iOS renders date/time inputs with a wide intrinsic size and its own
       internal padding; clamp them so they stay inside their flex column. */
    .form-field input[type="date"] {
      max-width: 100%;
      min-width: 0;
      -webkit-appearance: none;
      appearance: none;
      padding-right: 8px;
    }

    .location-row {
      display: flex;
      flex-direction: column;
      gap: 8px;
    }

    .btn-location {
      width: 100%;
      padding: 11px;
      border-radius: 10px;
      border: 1px solid rgba(0, 242, 254, 0.3);
      background: rgba(0, 242, 254, 0.08);
      color: var(--primary);
      font-size: 0.85rem;
      font-weight: 600;
      cursor: pointer;
      transition: all 0.2s ease;
    }

    .btn-location:hover:not(:disabled) {
      background: rgba(0, 242, 254, 0.15);
    }

    .btn-location:disabled {
      opacity: 0.5;
      cursor: default;
    }

    .location-status {
      font-size: 0.75rem;
      color: var(--text-muted);
      min-height: 1em;
      text-align: center;
    }

    .form-section {
      border: 1px solid var(--border);
      border-radius: 12px;
      overflow: hidden;
      background: rgba(255, 255, 255, 0.02);
    }

    .form-section > summary {
      list-style: none;
      cursor: pointer;
      padding: 12px 14px;
      font-size: 0.82rem;
      font-weight: 700;
      color: var(--text);
      display: flex;
      justify-content: space-between;
      align-items: center;
      user-select: none;
    }

    .form-section > summary::-webkit-details-marker { display: none; }

    .form-section > summary::after {
      content: '＋';
      color: var(--text-muted);
      font-weight: 400;
    }

    .form-section[open] > summary::after { content: '－'; }

    .form-section[open] > summary {
      border-bottom: 1px solid var(--border);
    }

    .form-section .section-body {
      padding: 14px;
      display: flex;
      flex-direction: column;
      gap: 14px;
    }

    .form-check-row {
      display: flex;
      gap: 16px;
      flex-wrap: wrap;
    }

    .form-check {
      display: flex;
      align-items: center;
      gap: 6px;
      font-size: 0.85rem;
      color: var(--text);
      cursor: pointer;
    }

    .form-check input { width: auto; }

    .form-readout {
      display: flex;
      flex-wrap: wrap;
      gap: 6px;
    }

    .form-actions {
      display: flex;
      gap: 10px;
      margin-top: 4px;
    }

    .form-actions .btn-primary { flex: 1; }

    .btn-secondary {
      flex: 1;
      padding: 10px 16px;
      border-radius: 10px;
      border: 1px solid var(--border);
      background: transparent;
      color: var(--text-muted);
      font-size: 0.85rem;
      font-weight: 600;
      cursor: pointer;
    }

    .btn-secondary:hover { color: var(--text); border-color: var(--primary); }

    .btn-primary:disabled,
    .btn-secondary:disabled {
      opacity: 0.5;
      cursor: default;
    }

    /* --- Profile: backup / restore --- */
    .data-blurb {
      font-size: 0.85rem;
      line-height: 1.5;
      color: var(--text-muted);
      margin-bottom: 10px;
    }

    .data-count {
      font-size: 0.8rem;
      font-weight: 700;
      color: var(--text);
      margin-bottom: 14px;
      min-height: 1em;
    }

    .data-status {
      margin-top: 12px;
      min-height: 1em;
      font-size: 0.78rem;
      color: var(--text-muted);
      text-align: center;
    }

    .data-status.error { color: var(--danger); }

    .form-check-col {
      display: flex;
      flex-direction: column;
      gap: 8px;
    }

    #import-result {
      margin-top: 16px;
      font-size: 0.82rem;
      line-height: 1.6;
      color: var(--text-muted);
    }

    #import-result.error { color: var(--danger); }

    @media (max-width: 400px) {
      .results-grid {
        grid-template-columns: 1fr;
      }
      .form-row {
        flex-direction: column;
      }
    }

/* =============================================================================
   ⭐⭐ THE DESKTOP LAYOUT — T-183, and the owner's report is the whole specification:
   *"in the profile page, why do you try to make this mobile compatible? i dont need it to be. make it look
   normal. same with the other pages where you try to stretch buttons. it looks weird and i dont like it.
   its not needed"*.

   He is right, and the cause is structural rather than careless: everything above the "web-only additions"
   marker in this file is a BYTE-FOR-BYTE extraction of the phone app's `<style>` block, and a phone has one
   column and full-width buttons because it is 390 pixels wide. Served at 1400 pixels that becomes a single
   890px column of 860px-wide inputs with three buttons stretched across it — measured on a screenshot
   before any of this was written.

   ## ⚠️⚠️ EVERY RULE HERE IS INSIDE A `min-width` QUERY, AND THAT IS NOT HEDGING

   The rules above are the app's own stylesheet, ported rather than authored here, and they are pinned: an
   md5 tripwire in `api/tests/run.php`'s app.css section goes red if the copy changes. (T-517: it does NOT
   exit 2 out of `native/differential/differential.mjs` — that tool pins `www/index.html` and never reads
   site/ at all, which is what this docblock used to claim.) So this is an override block, and
   scoping it to wide viewports is what makes it an ADDITION rather than a replacement — the phone layout is
   still exactly the phone layout, which matters because the same CSS is what the app itself uses.

   ⚠️ It is NOT "keeping it mobile compatible" in the sense the owner objected to. What he objected to is
   the DESKTOP page looking like a phone, and that is what these rules fix. Deleting the narrow layout as
   well would change the app's own appearance and was not asked for.
   ============================================================================= */

/* 860px rather than a round 900: the app shell's content column is about 890px at any width above it, so
   this is the point at which two form columns each get a sensible width instead of two cramped ones. */
@media (min-width: 860px) {
  /* ---- ⭐ BUTTONS STOP STRETCHING -------------------------------------------------------------
     `.form-actions .btn-primary { flex: 1 }` and `.btn-secondary { flex: 1 }` are in the copied region, so
     every action row spreads its buttons across the full width of whatever contains it. On a phone that is
     right — a thumb wants a big target and there is one column. On a desktop it makes a Save button look
     like a page banner.
     ⚠️ `min-width` rather than a fixed width, so a long label ("Cancel it or reopen the account…") still
     fits and a short one ("Save") is not a stripe. */
  .form-actions {
    justify-content: flex-start;
    flex-wrap: wrap;
  }
  .form-actions .btn-primary,
  .form-actions .btn-secondary,
  .form-actions .btn-danger {
    flex: 0 0 auto;
    min-width: 150px;
  }
  /* ⚠️ The LOGIN button is deliberately left alone: it carries `style="width:100%"` inline, which wins
     over this block, and a full-width Sign in inside a narrow login card is the conventional shape rather
     than the phone layout leaking out. Recorded so nobody "fixes" it to match. */

  /* ---- ⭐⭐ THE PROFILE FORMS IN TWO COLUMNS ---------------------------------------------------
     `.dive-form` is `flex-direction: column` in the copied region, so one field per row at any width. A
     grid on these three specific forms turns that into two columns without touching a line of markup.
     ⚠️ `minmax(0, 1fr)` and not `1fr`: a bare `1fr` refuses to shrink below its content's intrinsic width,
     which is the same trap `.form-field`'s own `min-width: 0` comment records for native date inputs. */
  #profile-form,
  #equip-form,
  #password-form {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 14px 18px;
    align-items: start;
  }
  /* Anything that is not a single field spans both columns: the action row, the notes, and a `.form-row`
     (which is already its own flex pair and would otherwise be squeezed into one cell). */
  #profile-form > *:not(.form-field),
  #equip-form > *:not(.form-field),
  #password-form > *:not(.form-field) {
    grid-column: 1 / -1;
  }
  /* ⚠️ The dive-profile NAME spans both, because it is the one required field and the thing every other
     field on that form describes — half a row would read as optional. */
  #equip-form > .form-field:first-child {
    grid-column: 1 / -1;
  }

  /* ---- ⭐ THE TOOLS TAB'S FULL-WIDTH CONTROLS -------------------------------------------------
     Measured with the button audit rather than by eye: at 1400px the two segmented toggles gave each half
     350px, and `Acronym Key` was a 710px stripe. Both are in the copied region, where they are right — a
     segmented control fills a phone's width and a thumb wants the whole row.
     ⚠️ The TOGGLE is capped rather than shrunk to its content: it is a two-state control whose halves must
     stay equal, and content-sized segments would make "Imperial (fsw)" wider than "Metric (msw)" and the
     active half jump size as you switch. A cap keeps them equal and stops them being a banner. */
  .unit-toggle {
    max-width: 420px;
  }
  /* ⚠️ `.key-trigger` is a link-ish secondary action, not a form submit, so it gets content width rather
     than the 150px floor the action buttons get — a 150px minimum on the word "Acronym Key" does nothing,
     and on a shorter label it would look like a form button, which this is not. */
  .key-trigger {
    display: inline-block;
    width: auto;
  }

  /* ---- The dive form's sections ---------------------------------------------------------------
     These already pair their fields with `.form-row`, so they need only the button fix above. What they
     DO need is for a lone field in a section not to run the full width beside nothing. */
  .form-section .section-body > .form-field {
    max-width: 48%;
  }
  /* ⚠️ Except the ones that are genuinely long — a note, and the preset picker with its button. */
  .form-section .section-body > .form-field.dive-notes-field,
  .form-section .section-body > #dive-equip-row {
    max-width: none;
  }
}

/* =============================================================================
   Web-only additions below this line.

   ⚠️ ABOVE THIS LINE IS NOT A PURE COPY — though the copy inside it is intact. www/index.html's <style>
   block is still byte-for-byte inside the region above, all but the two spaces of indentation before its
   closing tag: 17,904 of its 17,906 characters, ENDING AT LINE 818. Lines 819-925 are T-183's desktop
   `@media (min-width: 860px)` block and its docblock — 107 lines the app does not have, appended here
   instead of below this marker, which makes the region a SUPERSET of the app's own stylesheet. Two
   consequences worth knowing before touching it: re-extracting the region from www/index.html deletes those
   107 shipped lines, and nothing in this repo sha256-pins the region either way
   (native/differential/differential.mjs pins www/index.html and never reads site/ at all).

   Add anything new BELOW this line. Measured and corrected on T-517, which also deleted this comment's
   claim that the region above was untouched — a sentence that was false and load-bearing.
   ============================================================================= */

/* --- `hidden` must actually hide. THIS RULE MUST STAY FIRST. ----------------
   The web build has two full-page screens, `#login-screen` (.login-shell) and
   `#app-screen` (.app-shell), and it switches between them with the `hidden`
   attribute. `hidden` is only hidden because the UA stylesheet says
   `[hidden] { display: none }` — and that is an ORIGIN-level rule with a
   specificity of one attribute selector. `.app-shell { display: flex }` (in the
   copied region above) and `.login-shell { display: flex }` (below) both beat it,
   so without this rule NEITHER screen is ever hidden: both render, stacked, each
   `min-height: 100vh`. Measured before the fix: after a successful login the app
   shell started at y=844 on a 390x844 viewport, the first dive card at y=1052 —
   a full viewport below the fold — and `elementFromPoint(centre)` was the login
   email input. Signed out, the whole app shell (bottom nav, "Sign out", the
   nitrox calculator) rendered below the login card and was tappable.

   `!important` is the correct tool here rather than a wart to apologise for. The
   entire requirement is "this must beat any author rule", including rules in the
   frozen copied region that this file is not allowed to touch and any future one
   added above. A specificity ladder (`.app-shell[hidden]`, `.login-shell[hidden]`)
   would work today and would silently lose to the next `display` declaration on a
   screen element; a class-based toggle of our own would mean editing every
   `hidden` read/write in index.html to a mechanism a reader has to learn, and
   would leave the platform attribute still lying about what it does.

   ## Why this lives DOWN HERE and not next to `.app-shell`
   Everything above the marker is a byte-identical extraction of www/index.html's
   `<style>` block, pinned by an md5 tripwire in api/tests/run.php's app.css
   section — NOT by native/differential/differential.mjs, which pins
   www/index.html and never reads site/ (T-517). Editing the copied region — even to add one correct rule — makes the
   two undiffable and is how the web and native styles start to drift silently.
   www/index.html has no `hidden` attribute and no login screen, so this is a
   web-only defect and the frozen reference needs no change. -------------------- */
[hidden] { display: none !important; }

/* --- The login screen -------------------------------------------------------
   Only the web build has one: the native app holds its token in the Keychain,
   so it never shows this. Reuses .calculator-card and the .form-field rules
   already defined above rather than inventing a second visual language. */
.login-shell {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  padding: max(16px, env(safe-area-inset-top)) 16px;
}

.login-card {
  width: 100%;
  max-width: 400px;
}

.login-blurb {
  color: var(--text-muted);
  font-size: 0.85rem;
  line-height: 1.5;
  margin-bottom: 20px;
  text-align: center;
}

/* Native inputs in a flex column need this or a long placeholder pushes the
   card wider than the phone — the same rule the dive form relies on. */
.login-card .form-field { min-width: 0; }

.login-error {
  background: rgba(239, 68, 68, 0.12);
  border: 1px solid rgba(239, 68, 68, 0.4);
  color: #fca5a5;
  border-radius: 12px;
  padding: 10px 12px;
  font-size: 0.8rem;
  margin-bottom: 14px;
  /* A server message is inserted here with escapeHtml; wrapping stops a long
     one from widening the card. */
  overflow-wrap: anywhere;
}

.login-foot {
  margin-top: 18px;
  text-align: center;
  font-size: 0.78rem;
  color: var(--text-muted);
  line-height: 1.6;
}

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

/* ⭐ The "Forgot your password?" control. A BUTTON that reads as a link — see the comment on the markup
   in site/index.html for why it is not an anchor.
   ⚠️ `appearance: none` and an explicit `background: none`: a bare `<button>` inherits a grey chrome
   button on every platform, and on Safari a border and a corner radius as well, so the four resets below
   are all load-bearing rather than defensive. `font: inherit` because a button does NOT inherit the
   page's font family, which is what makes an unstyled one look pasted in. */
.login-aside { margin-top: 12px; text-align: center; }
.login-link {
  appearance: none; -webkit-appearance: none;
  background: none; border: 0; padding: 0; margin: 0;
  font: inherit; font-size: 0.8rem;
  color: var(--primary); cursor: pointer; text-decoration: underline;
  text-underline-offset: 2px;
}
.login-link:hover { color: var(--text); }
/* ⚠️ A visible ring, because removing the border removed the only thing a keyboard user could see. */
.login-link:focus-visible { outline: 2px solid var(--primary); outline-offset: 3px; border-radius: 4px; }
.login-link[disabled] { opacity: 0.6; cursor: default; text-decoration: none; }

/* The neutral answer to a reset request, in the login card. Reuses `.read-only-note`'s cyan panel — the
   sentence is information rather than a problem, so `.login-error`'s red would be wrong — and only needs
   the gap the card does not already give it. */
#forgot-note { margin-top: 14px; font-size: 0.78rem; text-align: left; }

/* ===== ⭐⭐ DIVE PROFILES (equipment presets) — T-182 =========================================
   One row per preset: name and a summary on the left, Edit and Delete on the right. A flex row rather
   than a table, because there is one action per entry and a table would imply columns that do not exist —
   the same reasoning the admin console's match list records. */
.equip-list { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; }
.equip-row {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 10px 12px; border: 1px solid var(--border); border-radius: 12px;
  background: rgba(255, 255, 255, 0.03);
}
/* `min-width: 0` so a long preset name wraps instead of pushing the buttons off the card — the rule
   `.login-card .form-field` already needed for the same reason. */
.equip-main { min-width: 0; }
.equip-name {
  font-size: 0.9rem; font-weight: 600; color: var(--text);
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
}
.equip-sub { font-size: 0.78rem; color: var(--text-muted); margin-top: 2px; overflow-wrap: anywhere; }
/* The default marker. Cyan, which on this site means "switched on for this account". */
.equip-badge {
  font-size: 0.64rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--primary); border: 1px solid var(--primary); border-radius: 999px;
  padding: 1px 7px; background: var(--primary-glow);
}
.equip-actions { display: flex; gap: 6px; flex: 0 0 auto; }
/* `flex: 0 0 auto` because `.btn-secondary` is `flex: 1` in the copied region — right inside a form's
   action row, wrong here, where two stretched buttons would read as the card's primary action. */
.equip-btn { flex: 0 0 auto; padding: 6px 12px; font-size: 0.78rem; min-width: 0; }
.equip-del { color: #fca5a5; }
/* The "(required)" / "(used for SAC)" hints inside a label. Dimmer than the label, same line. */
.equip-req { color: var(--text-muted); font-weight: 400; font-size: 0.74rem; }
/* A checkbox and its label on one line — `.form-field` is a column everywhere else. */
.equip-default-row { flex-direction: row; align-items: center; gap: 8px; }
.equip-default-row label { margin: 0; }

/* The dive form's preset picker: a select and its button side by side. */
.dive-equip-pick { display: flex; gap: 8px; align-items: center; }
.dive-equip-pick select { flex: 1 1 auto; min-width: 0; }

.read-only-note {
  background: rgba(56, 189, 248, 0.08);
  border: 1px solid rgba(56, 189, 248, 0.25);
  border-radius: 12px;
  padding: 10px 12px;
  font-size: 0.78rem;
  color: var(--text-muted);
  margin-bottom: 14px;
}

.logbook-header .btn-secondary { flex: 0 0 auto; }

/* --- The TOP HEADER BAR, and the bottom tab bar it replaces ------------------
   The web app navigates from a sticky header: wordmark left, three text tabs
   right, an underline under the active one. The PHONE app still uses the bottom
   bar — www/index.html is frozen and this is a web-only change.

   ## ⚠️ Why the `.bottom-nav` override is DOWN HERE and not next to the rule
   `.bottom-nav`, `.nav-btn`, `.nav-btn .nav-icon` and `.nav-btn.active` are at
   roughly lines 97-134 of this file, which is INSIDE the byte-for-byte extraction
   of www/index.html's `<style>` block (everything above the "web-only additions"
   marker). That copy is pinned by an md5 tripwire in api/tests/run.php's app.css
   section; native/differential/differential.mjs pins www/index.html itself and
   never reads site/ (T-517) — so deleting the rules there would force an edit to
   the frozen reference, and deleting them HERE would break the byte-identity the
   marker promises and make the two files undiffable. The same reasoning as the
   `[hidden]` rule at the top of this section, for the same region.

   So the four bottom-bar rules stay exactly where they are and are now DEAD on the
   web: site/index.html no longer renders a `.bottom-nav` at all (the markup was
   deleted, which is why there is no `display: none` here — there is nothing left
   to hide). The one line that override WOULD have been is replaced by the one
   below, which undoes the space the bar used to occupy. --------------------------

   `.app-content`'s bottom padding is `calc(72px + env(safe-area-inset-bottom))` in
   the copied region — a gap reserved for a fixed bar that no longer exists, and
   without this it is 72px of dead air under the last dive card. The top padding is
   re-declared for the opposite reason: it was `max(16px, env(safe-area-inset-top))`
   because the content used to be the top-most thing on screen, and the HEADER now
   owns the notch inset. Content only needs a gap under the bar. */
.app-content {
  padding-top: 16px;
  padding-bottom: max(24px, env(safe-area-inset-bottom));
}

/* `sticky`, not `fixed`. Both stay visible while the page scrolls; only sticky
   participates in layout, so it reserves its own height and CANNOT overlap the
   first dive card. A `position: fixed` bar would need its height duplicated as a
   top padding somewhere else, and the two would drift the moment the wordmark's
   font-size changed. This is also why it is a sibling of `.app-content` rather
   than inside it: sticky binds to the nearest scrolling ancestor, and what
   actually scrolls in a browser here is the DOCUMENT (index.html's appScrollTop
   comment records the measurement).

   The surface is the same treatment `.bottom-nav` used — the same translucent
   `--bg`, the same blur, `--border` for the hairline — flipped to a bottom edge.
   No new colours. */
.top-bar {
  position: sticky;
  top: 0;
  z-index: 50;   /* Same layer the bottom bar used. Under .modal-overlay's 100. */
  background: rgba(11, 15, 25, 0.85);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  border-bottom: 1px solid var(--border);
  /* Standalone / notched iOS: the bar is now the TOP-most element, so it owns the
     status-bar inset that `.app-content` used to claim. UNVERIFIED on a device —
     there is no notch in a desktop browser to test it against; env() resolves to
     0px there, which is why the desktop layout is unaffected either way. */
  padding-top: env(safe-area-inset-top);
}

/* Matches `.content-inner`'s 500px column and `.app-content`'s side padding, so
   the wordmark lines up with the dive cards under it instead of floating off at
   the window edge. The 900px widening below keeps them aligned at desktop too. */
.top-inner {
  width: 100%;
  max-width: 500px;
  margin: 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  /* Longhand deliberately, not the four-value `padding` shorthand: written as a
     shorthand this read `8px <right> 0 <left>` and the two insets were transposed —
     inset-LEFT applied to the right edge. Portrait makes both 0 so nothing showed it,
     and a landscape notch is where it would have surfaced. Longhand cannot be
     transposed. */
  padding-top: 8px;
  padding-bottom: 0;
  padding-right: max(12px, env(safe-area-inset-right));
  padding-left: max(12px, env(safe-area-inset-left));
}

/* The wordmark, absorbed from the `.brand-header` that used to sit in
   `.content-inner`. Same gradient-clipped treatment as `.brand-header h1`, at a
   header-bar size. `clamp()` rather than a flat size for the reason site/README
   records about the marketing pages: at 320px a flat wordmark plus three labels
   overflows, and shrinking the word is what pays the row's budget back.
   `flex-shrink: 0` would reintroduce exactly that overflow. */
.top-brand {
  /* The FLOOR is the load-bearing number, not the ceiling. At a 0.95rem floor the word
     measured 87.9px against exactly 87.9px of room at 320 — zero slack — and at 280 the
     box shrank to 81px and CUT THE GLYPHS OFF (the media query below records why that
     failure is invisible to an overflow test). 0.78rem lets the word get smaller instead
     of getting cut.

     `4.2vw` governs from ~297px upward, so the floor changes nothing at 320 or above:
     the wordmark is 13.44px at 320 either way. What it bought, measured, is the gap
     between the wordmark and the first tab at 320 — 22.5px before, 39.6px now — and no
     clipping at any width from 280 up. */
  font-size: clamp(0.78rem, 4.2vw, 1.2rem);
  font-weight: 800;
  letter-spacing: 0.02em;
  background: linear-gradient(135deg, var(--primary), var(--accent));
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  /* One word, so it cannot wrap; it must be allowed to shrink instead. */
  min-width: 0;
  white-space: nowrap;
}

.top-actions {
  display: flex;
  align-items: center;
  gap: 4px;
  min-width: 0;
}

.top-nav {
  display: flex;
  align-items: stretch;
  min-width: 0;
}

/* A real `<button>`, so Enter and Space activate it and Tab reaches it with no
   JavaScript and no `tabindex` — the same reason the dive cards needed a
   `keydown` handler and these do not. */
.top-tab {
  background: none;
  border: none;
  color: var(--text-muted);
  font-size: clamp(0.72rem, 3.2vw, 0.85rem);
  font-weight: 600;
  cursor: pointer;
  padding: 10px 6px 8px;
  /* The underline is drawn as a transparent border that becomes visible, rather
     than appearing from nothing: a 2px border added on activation would shift the
     label 2px upward every time a tab is tapped. */
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;   /* Sits on the bar's own hairline, not above it. */
  white-space: nowrap;
  transition: color 0.15s ease, border-color 0.15s ease;
}

.top-tab:hover { color: var(--text); }

/* TWO signals, deliberately: the colour AND the underline. Colour alone fails a
   diver with low colour vision, and this bar has no icons left to carry a shape
   difference. `font-weight` is NOT part of the active state — a bolder label is
   wider, which would reflow the row on every tab tap and is what makes the
   underline shift under a neighbouring word. */
.top-tab.active {
  color: var(--primary);
  border-bottom-color: var(--primary);
}

/* `:focus-visible`, not `:focus` — the same choice `.dive-card` makes above, so a
   mouse click does not leave a ring behind while a keyboard user always sees where
   they are. Without this the ring is the UA default, which on a dark translucent
   bar is very nearly invisible. */
.top-tab:focus-visible,
.top-signout:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
  border-radius: 6px;
}

/* Sign out reuses `.btn-secondary` (copied region) so it is the same button the
   app already had in two places, just smaller and no longer stretching: that rule
   sets `flex: 1`, which in this row would make it eat the space the tabs need.

   ⚠️ DEAD as of the username menu: `site/index.html` no longer renders a
   `.top-signout` — Sign out is an item inside `.top-user-menu` now. Kept rather
   than deleted because it is three declarations and the class name is the obvious
   one for any future header button; the `:focus-visible` rule above it still
   names it too. If a second header button never appears, delete both. */
.top-signout {
  flex: 0 0 auto;
  padding: 6px 10px;
  font-size: clamp(0.68rem, 3vw, 0.78rem);
  white-space: nowrap;
}

/* --- The two header dropdowns: the username menu, and Tools -----------------
   The username menu replaced the bare "Sign out" button: the diver's NAME is the
   trigger, and the menu behind it holds Profile and Sign out. Profile left the tab
   row entirely — a tab AND a menu item for one destination is the duplication that
   was removed when two Sign out buttons became one.

   **Tools is now the same kind of control** ("i like the dropdown in the menu like
   Profile has for tools instead of where you select now....looks tacky"), so the
   surface, the item treatment and the chevron are SHARED rather than copied — the
   selector lists below are how, and the block at the end of this section is what
   Tools adds. The web-only-additions marker is above all of it, so nothing in the
   frozen copied region is touched.

   `position: relative` on the wrapper is what the absolutely-positioned menu
   anchors to. `min-width: 0` for the reason every flex child in this file needs
   it: without it the trigger's intrinsic width (a 40-character email) becomes the
   row's minimum and the header overflows the phone instead of ellipsising. */
.top-user {
  position: relative;
  display: flex;
  align-items: center;
  min-width: 0;
  flex: 0 1 auto;   /* May shrink; must not grow into the space the tabs use. */
}

/* Not `.btn-secondary`: this is a menu button, not an action, and it sits in a row
   where a bordered box beside two borderless tabs reads as the odd one out. It
   takes `.top-tab`'s type treatment instead — same font-size clamp, same muted
   colour, same 2px transparent underline — so the header has one visual language
   and the underline can mark the profile route the same way it marks a tab. */
.top-user-trigger {
  display: flex;
  align-items: center;
  gap: 5px;
  min-width: 0;
  /* ## `min(42vw, 200px)`, and the 200px is a MEASURED bug fix, not a round number
     `42vw` alone was wrong at tablet widths and it failed in the way this file already
     warns about: the wordmark is `min-width: 0` and `nowrap`, so when the row's content
     exceeds the 500px column the flexbox does not overflow the document — it SHRINKS both
     children in proportion to their base widths and the wordmark's GLYPHS get cut off
     inside its box. `scrollWidth == clientWidth` still passes.

     Measured at 768 with a 40-character email before this cap (a Range around the text
     node, which is the only way to see it):

       | trigger cap | wordmark glyphs | its box | trigger | last tab right / trigger left |
       |-------------|-----------------|---------|---------|-------------------------------|
       | 42vw = 322  | 108.5px         | 99.1px  | 278.2px | 348.2 / 343.8  OVERLAPPED     |
       | min(…,200)  | 108.5px         | 108.5px | 200.0px | 348.2 / 421.8  clear          |

     42vw still governs every phone width — it is 134.4 at 320, 157.5 at 375 and 173.9 at
     414, all below 200 — so the cap changes nothing there and only stops the label from
     spending the row's whole budget once a viewport is wide enough for 42vw to exceed what
     the 500px column can afford. The wide-screen block raises it again where there is
     genuinely room. */
  max-width: min(42vw, 200px);
  background: none;
  border: none;
  color: var(--text-muted);
  font-size: clamp(0.72rem, 3.2vw, 0.85rem);
  font-weight: 600;
  cursor: pointer;
  padding: 10px 4px 8px;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;   /* On the bar's hairline, like `.top-tab`. */
  transition: color 0.15s ease, border-color 0.15s ease;
}

.top-user-trigger:hover { color: var(--text); }

/* The profile route has no tab button any more, so the trigger carries its active
   state — colour AND underline, the same two signals `.top-tab.active` uses, for
   the same reason (colour alone fails a diver with low colour vision). */
.top-user-trigger.active {
  color: var(--primary);
  border-bottom-color: var(--primary);
}

.top-user-trigger:focus-visible,
.top-user-item:focus-visible,
.top-menu-item:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
  border-radius: 6px;
}

/* The label is the shrinkable part. A 40-character email cannot fit a 320px header
   beside a wordmark and two tabs, so it ellipsises and the full value lives in the
   trigger's `title` (set in index.html alongside the text, so the two cannot drift).

   `min-width: 0` plus `overflow: hidden` is what actually permits the shrink —
   `text-overflow` alone does nothing to a flex child that refuses to get smaller
   than its content. */
.top-user-label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* A CSS triangle, not an icon font and not "▾": no external request is permitted
   here, and the glyph's weight and baseline differ on every platform — the same
   reason the bottom bar's per-tab emoji were dropped. `currentColor` so it tracks
   the hover and active colours without repeating either.

   `flex-shrink: 0` because this one must NOT shrink: it is 5px wide and the label
   beside it is what should give way.

   ⚠️ It is 8px WIDE, not 5: 4+4px of left and right border. The 5px is its height.
   That distinction is what the Tools chevron's space budget is written against. */
.top-user-chevron,
.top-menu-chevron {
  flex: 0 0 auto;
  width: 0;
  height: 0;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-top: 5px solid currentColor;
  transition: transform 0.15s ease;
}

/* Points up while the menu is open — the same fact `aria-expanded` carries, for
   people who can see it. `[aria-expanded]` is read straight off the attribute the
   JS already maintains, so there is no third piece of state to keep in step. */
.top-user-trigger[aria-expanded="true"] .top-user-chevron,
.top-menu-trigger[aria-expanded="true"] .top-menu-chevron {
  transform: rotate(180deg);
}

/* The menu. Takes the modal's surface language from the copied region — `--card-bg`
   for the tint, `--border` for the hairline, the same deep shadow — at a dropdown's
   radius, so it looks like the app rather than a second design.

   `right: 0`, not `left: 0`: it hangs off the right edge of the trigger, which is
   itself at the right edge of the viewport. Anchored left, a 140px menu on a
   320px screen would run off the side of the phone.

   z-index sits above `.top-bar`'s own 50 and below `.modal-overlay`'s 100, so the
   Acronym Key modal still covers it.

   ## ⚠️ OPAQUE, and that is a legibility fix found in a screenshot
   `background: var(--card-bg)` alone — which is what `.modal` and
   `.calculator-card` use — is `rgba(17, 24, 39, 0.75)`, i.e. 25% transparent. That
   works for a modal because `.modal-overlay` puts a dark scrim behind it, and for a
   card because the thing behind a card is the page background. A DROPDOWN has
   neither: it floats directly over content. Measured at 390px with the menu open,
   the "Read-only on the web for now — add, edit and delete dives in the iOS app."
   note was legible THROUGH the menu, with its words running across "Sign out".
   `backdrop-filter: blur()` did not save it: blur smears the text without reducing
   its contrast enough to stop it reading as text.

   The fix composites the same tint over an opaque `var(--bg)` base rather than
   introducing a colour: two background layers, `--card-bg` painted on top of
   `--bg`, both existing custom properties. The result is fully opaque, so
   `backdrop-filter` is removed with it — it would cost a compositing layer to blur
   something nothing can see through. Verified by capturing the menu's pixels with
   and without content underneath and requiring them IDENTICAL.

   `.top-menu-list` (Tools) shares every one of these declarations. `right: 0` is
   right for it too even though it is not at the right edge: the Tools trigger sits
   left of the username trigger, so a menu anchored to its LEFT edge would still fit
   at 320 — but anchored right it also cannot reach the viewport edge, and one rule
   for both means the two dropdowns cannot end up with different geometry. */
.top-user-menu,
.top-menu-list {
  position: absolute;
  top: calc(100% + 4px);
  right: 0;
  z-index: 60;
  min-width: 140px;
  padding: 6px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  background: linear-gradient(var(--card-bg), var(--card-bg)), var(--bg);
  border: 1px solid var(--border);
  border-radius: 14px;
  box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5);
}

/* Real `<button>`s, so Enter, Space and Tab work with no `tabindex` and no keydown
   shim. `text-align: left` because a button centres its text by default and a
   two-item menu of centred labels reads as a pair of pills. */
.top-user-item,
.top-menu-item {
  background: none;
  border: none;
  border-radius: 10px;
  color: var(--text);
  font-size: 0.85rem;
  font-weight: 600;
  text-align: left;
  padding: 9px 12px;
  cursor: pointer;
  white-space: nowrap;
  transition: background 0.15s ease, color 0.15s ease;
}

.top-user-item:hover,
.top-menu-item:hover {
  background: rgba(255, 255, 255, 0.06);
  color: var(--primary);
}

/* --- What TOOLS adds on top of the shared menu ------------------------------
   The wrapper. Same job `.top-user` does: `position: relative` is what the
   absolutely-positioned list anchors to. `align-items: stretch` because `.top-nav`
   is `align-items: stretch` and the trigger is now one level deeper than
   `#nav-dives` — without it the button would size to its content inside a
   stretched wrapper and its underline would sit at a different height from the
   Dives tab's. (The file records a 4.2px skew of exactly this kind, from the
   avatar. Measured after this rule: both underlines end at the same y at every
   width.) */
.top-menu {
  position: relative;
  display: flex;
  align-items: stretch;
  min-width: 0;
}

/* The trigger IS a `.top-tab` — it carries both classes, so it keeps the tab's type,
   its transparent-border underline, its hover and its `.active` state, and
   `markActiveTab()` marks it for `#/tools/*` with no change. This only turns it into
   a flex row so the chevron can sit beside the label.

   The gap is 4px where `.top-user-trigger`'s is 5: this row has no 5px to spare (see
   the sweep below) and the chevron is a solid triangle against a word rather than
   against a 22px circle, so it does not need the extra separation. */
.top-menu-trigger {
  display: flex;
  align-items: center;
  gap: 4px;
}

/* The open tool, marked inside the menu because the trigger says "Tools" at every
   width — the tool's own name does not fit that row (site/index.html's header
   comment has the numbers).

   TWO signals, the same rule `.top-tab.active` follows: the colour AND the tick.
   Colour alone fails a diver with low colour vision, and in a three-item list of
   identically-shaped rows there is nothing else to tell them apart.

   ## The gutter is on EVERY item, and the tick's box is always there
   `::before` exists on all three items and only its borders change colour, so
   choosing a different tool cannot reflow the list — exactly why `.top-tab`'s
   underline is a transparent border that becomes visible rather than a border that
   appears. Drawn with borders rather than "✓" for the reason `.top-user-chevron`
   is: no external request is permitted here and a glyph's weight and baseline
   differ on every platform. */
.top-menu-item {
  display: flex;
  align-items: center;
  gap: 8px;
}

.top-menu-item::before {
  content: "";
  flex: 0 0 auto;
  /* The tick is a rotated corner: the box is square, two of its borders are drawn,
     and the -45° turn makes them a check. Sized so the whole mark fits the cap
     height of a 0.85rem label. */
  width: 9px;
  height: 5px;
  border-left: 2px solid transparent;
  border-bottom: 2px solid transparent;
  transform: rotate(-45deg) translate(1px, -1px);
}

.top-menu-item.current {
  color: var(--primary);
}

.top-menu-item.current::before {
  border-left-color: currentColor;
  border-bottom-color: currentColor;
}

/* --- WHAT THE TOOLS CHEVRON COST, AND WHERE THE SPACE CAME FROM -------------
   ⚠️ Read this before touching any horizontal value in the header row.

   The chevron is **12px**: 8px of triangle (4+4px of left/right border — the 5px in
   `.top-user-chevron` is its HEIGHT, which is the easy misreading) plus the 4px gap
   in `.top-menu-trigger`. The row did not have 12px. Measured before the chevron
   existed, with a 40-character email as the username label:

     | width | slack (lockup → actions) | wordmark |
     |-------|--------------------------|----------|
     | 320   | 7.2px                    | fits     |
     | 375   | 14.5px                   | fits     |

   "Slack" is the `space-between` gap between `.top-lockup` and `.top-actions` — the
   row's real free space. It is NOT the 4px figure that a naive measurement reports
   between the last tab and the username trigger: that 4px is `.top-actions`'s own
   declared `gap` and is constant at every width, so it says nothing about slack.

   Adding the chevron with no compensation put the wordmark back into the failure this
   file already documents twice — the flexbox SQUEEZING its box and the browser clipping
   the glyphs inside it, while `scrollWidth == clientWidth` still passes. Measured with a
   Range around the text node, which is the only thing that can see it:

     | width | wordmark glyphs | its box | verdict |
     |-------|-----------------|---------|---------|
     | 320   | 78.8px          | 75.6px  | CLIPPED |
     | 375   | 90.7px          | 88.7px  | CLIPPED |
     | 414   | 98.9px          | 98.9px  | fit     |

   ## The sweep, cheapest lever first
   Two of the three declarations below cost NO text at all, so they were tried before
   spending any of the email's width. Same method, same 40-char label, slack at 320:

     | change                                             | 320 slack | email at 320 |
     |----------------------------------------------------|-----------|--------------|
     | nothing (chevron unpaid)                           | 4.0 CLIP  | 56.8px       |
     | + trigger padding 6→4px (2px under 360)            | 4.0 CLIP  | 57.5px       |
     | + `.top-actions` gap 4→2px under 360               | 4.0 CLIP  | 57.5px       |
     | + `.top-tab` padding 4→3px under 360               | 4.0 CLIP  | 57.5px       |
     | + `.top-inner` padding 8→6px under 360             | 5.2px     | 58.4px       |
     | + trigger cap 32vw → 30vw                          | 9.6px     | 52.0px       |
     | **+ trigger cap 32vw → 29vw**                      | **12.8px**| **48.8px**   |

   The free levers alone were not enough — they buy about 1px at 320, because the
   trigger's own padding and the row gap come off a flex child that the cap is already
   holding at its maximum. So the cap pays the rest, exactly as it did when the logo
   arrived (see `.top-user-trigger { max-width: min(32vw, 200px) }` above and its table).

   **29 rather than 30**, and the reason is the one this file states about 33-vs-32: 30
   leaves 9.6px at 320 while the header it replaces had 7.2px, which is "it fits" with no
   margin for a different font metric. 29 leaves **12.8px — more slack than the row had
   BEFORE the chevron existed** — and costs 9.6px of visible email at 320 against the
   pre-change 58.4. The full address is still in the trigger's `title`, still ellipsised
   rather than cut, and from 768 up the `200px` arm governs and none of this applies (the
   900px block raises it to 320px, untouched).

   ## Verified, after, at every width in the brief, with both menus open and closed
   No clipped glyph on the wordmark, either tab label, or any of the five menu items, and
   `scrollWidth == clientWidth`, at 320 / 375 / 414 / 768 / 1400 — plus 359/360 (the media
   query's own edges) and 500.

   ⚠️ 280 and 300 STILL CLIP THE WORDMARK, and that is PRE-EXISTING, not this change: the
   same measurement against git HEAD's app.css and header markup clips at 280 (+7.3px) and
   300 (+2.3px) too. The `.top-brand` comment above claims 280 fits, and that claim was
   true before the logo and the avatar were added and is now stale. Neither width is in the
   brief's list and no phone ships at either, so it is recorded here rather than fixed
   under cover of a menu change.

   ⚠️ The CAP half of this fix is NOT here. `min(32vw, 200px)` is declared further down
   this file (the "trigger's cap comes down, because the row now carries a logo" block),
   and a rule up here would LOSE to it — same selector, same specificity, later wins. So
   the 29vw override sits immediately after that rule, where the cascade puts it in
   effect and where its own table is one screen away. */
.top-menu-trigger {
  padding-left: 4px;
  padding-right: 4px;
}

/* The logbook header is now just a heading — its "Sign out" moved to the bar — so
   the `space-between` in the copied region has nothing to push apart. Left as is;
   a single flex child sits at the start either way. This only recovers the space
   the deleted `.brand-header` used to hold off the top of the page. */
#tab-dives .logbook-header { margin-top: 4px; }

/* Under 360px the wordmark plus three labels plus Sign out is genuinely tight, even at
   the clamp's floor, so the horizontal padding tightens to buy the row ~32px. The tabs
   are NOT allowed to wrap — a two-line bar on the narrowest phone costs more vertical
   space than the emoji bar it replaced.

   ## The failure this exists to prevent is CLIPPING, which no overflow test can see
   `.top-brand` is `min-width: 0` and `nowrap`, so when the row runs out of room the
   flexbox does not overflow the document — it SQUEEZES the wordmark's box and the
   glyphs are cut off inside it. `scrollWidth == clientWidth` still passes; the page
   just says "SafetySto". Measured with a Range around the text node rather than the
   element box, which is the only way to see it:

     | width | wordmark text | its box  |               |
     |-------|---------------|----------|---------------|
     | 280   | 87.9px        | 81.0px   | CLIPPED       |
     | 320   | 87.9px        | 87.9px   | fit, 0 slack  |

   320 is the width this has to hold at and it did — with nothing to spare, which is
   the state site/README.md records as "the thing to notice, whoever's change finally
   spends it". These four declarations pay it back: 320 now carries ~30px of slack and
   280 fits too. Re-measured after: no clipping at 280 / 300 / 320 / 360 / 375 / 414 /
   768 / 1400.

   ⚠️ **That last sentence is now STALE for 280 and 300**, and it is recorded rather than
   quietly corrected because the sentence is the history. The logo and the avatar were
   added to this row after it was written and both were paid for at 320 (see their two
   `min(…vw, 200px)` blocks); measured against git HEAD, with a 40-character email as the
   label, the wordmark clips at 280 (+7.3px) and 300 (+2.3px) and has done since. 320 and
   up were and are clean. The Tools chevron did not change that either way — see its own
   block in the menu section for the before/after at both widths. */
@media (max-width: 359px) {
  .top-inner {
    gap: 4px;
    /* 6px, not the 8px this rule shipped with: 2px of side padding is part of what pays
       for the Tools chevron, and it costs no text at all. Measured contribution at 320:
       +1.2px of row slack. See the chevron's block in the menu section for the sweep. */
    padding-left: max(6px, env(safe-area-inset-left));
    padding-right: max(6px, env(safe-area-inset-right));
  }
  .top-tab { padding-left: 4px; padding-right: 4px; }
  .top-signout { padding-left: 6px; padding-right: 6px; }
  .top-user-trigger { padding-left: 2px; padding-right: 2px; }
  /* The Tools trigger is a `.top-tab` too, so the rule above already gives it 4px. This
     takes it to 2px, matching the username trigger beside it: both are menu triggers with
     a chevron, and at this width the chevron is what the padding is competing with. */
  .top-menu-trigger { padding-left: 2px; padding-right: 2px; }
  /* The mark shrinks with the row, and the gap closes: 26px of artwork plus an 8px gap is
     34px the wordmark and the tabs need more at 320 than the logo does. Measured at 320:
     the wordmark's glyphs are 78.8px in a 78.8px box (no clipping) with the logo present,
     which is the same text width the bar carried before the logo existed. */
  .top-lockup { gap: 5px; }
  .top-mark { width: 22px; height: 22px; }
  /* ⚠️ The avatar is NOT shrunk here, and the margin is NOT dropped. 320px is the width
     the sweep on `.top-avatar` was decided at — the tab is 33.5px there against 34 or 36
     everywhere else, so it has the least vertical headroom of any width and any change to
     these two declarations shows up as a taller bar and skewed underlines HERE first. The
     22px/-5px pair is what holds at 320; a smaller circle would be free but this one is
     already free. Overriding it would only make the face harder to see on the narrowest
     phone, which is where it is smallest already. */
}

/* --- The mark beside the wordmark -------------------------------------------
   The owner asked for "the safetystop logo next to the word". Same artwork as the
   app icon, the splash, the verification email (`api/src/Page.php`) and the
   marketing site — and specifically the SAME base64 data URI `site/about.html`
   inlines, byte-for-byte, so the app cannot drift from the marketing page and no
   external request is introduced. `site/README.md` and `_shared.css` both promise
   this page fetches nothing, and a logo from a CDN would break that promise for a
   16 KB image already in the bundle.

   ## The wrapper exists because the row's failure mode is CLIPPING, not overflow
   `.top-brand` and the mark are one lockup and have to shrink together. Without a
   wrapper they are two independent flex children of `.top-inner`, whose
   `justify-content: space-between` would have put a gap between the mark and the
   word it belongs to as soon as `.top-actions` stopped filling the row.

   `min-width: 0` on the wrapper for the reason every flex child in this file needs
   it: the mark is `flex: 0 0 auto`, so without it the wrapper's minimum is 26px of
   artwork PLUS the wordmark's intrinsic width and the shrink stops working — which
   is exactly the "SafetySto" clipping the 359px block above documents.

   ## 26px, and why the mark is NOT what constrains the bar
   Measured baseline bar heights, before either image existed: **41.5 / 42 / 44 / 44 /
   44** at 320 / 375 / 414 / 768 / 1400. A 26px mark changes none of them — it lives in
   `.top-lockup`, a sibling of `.top-actions`, and `.top-inner` is 49px of content box
   already. Proved by measurement rather than assumed: with the mark `display: none`d the
   bar was still 50px while the avatar was 22px unmargined, so the AVATAR was the whole
   cause of that regression and the mark contributed nothing. (See `.top-avatar`.)

   Compared at 22 / 26 / 30 against the real
   wordmark: at 30 the mark is visibly taller than the cap height of a 19.2px word
   and reads as a badge with a caption; 26 is about 1.35× the cap height, which is
   the ratio the marketing lockup uses (128px mark, 2rem word).

   `width`/`height` are ALSO on the element as attributes in index.html. That is
   not redundant: they give the box an intrinsic size before the CSS applies, so a
   slow first paint cannot lay the row out at zero width and then jump. */
.top-lockup {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}

.top-mark {
  flex: 0 0 auto;
  width: 26px;
  height: 26px;
  /* The source PNG is square (256x256 of a 1024 render), but `object-fit` is stated
     rather than assumed: if the artwork is ever replaced with a non-square export,
     this squashes nothing — it letterboxes inside the same 26px box, so the row's
     width budget cannot change underneath the measurements above. */
  object-fit: contain;
  /* Not `block`: the wrapper is a flex container, so the image is already blockified
     and no baseline gap exists to remove. Stated for the reader who reaches for the
     usual `display: block` fix and wonders why it is absent. */
}

/* --- The avatar beside the name ---------------------------------------------
   "maybe you can also add their uploaded avatar next to their name" — the diver's
   own photo, `diver_profiles.profile_photo`, fetched from `GET /session/avatar` as
   image bytes rather than base64 in a JSON body. `/session/me` deliberately does
   not carry it; see `api/src/Profiles.php`.

   ## A FIXED size on the container, occupied from the first paint
   This box is 22px whether it holds a photo, a letter, or nothing, and that is the
   whole no-layout-shift argument. The avatar arrives on a round trip that a 404 —
   the COMMON outcome, since most accounts have no photo — resolves just as slowly
   as a 200, so a box that sized itself to its content would move the wordmark and
   both tabs twice on every page load.

   `flex: 0 0 auto` for the same reason `.top-user-chevron` has it: at 22px this is
   not the thing that should give way when the row is tight. The LABEL shrinks —
   it is `min-width: 0` + `text-overflow: ellipsis` and a 40-character email is
   what it exists to absorb.

   ## The letter fallback, not a silhouette and not an empty circle
   No icon font and no SVG file is permitted here (no external requests), and an
   empty circle inside a button reads as an image that failed. So the slot carries
   the initial of whatever the label ended up being — a name, an email, or the
   pre-identity "Account" placeholder.

   Colours are the two existing custom properties the rest of the bar uses:
   `--primary` on the same 6% white wash `.top-user-item:hover` uses. No new colour.

   `overflow: hidden` plus `border-radius: 50%` is what actually rounds the photo —
   the `<img>` inside is square and is clipped by this box, so nothing depends on
   the stored image having been cropped round on the device.

   ## ⚠️ The negative block margin is LOAD-BEARING. It is what keeps the bar 44px tall.
   A plain 22px circle grows the header from **41.5px to 50px** and, worse, breaks an
   invariant this file states twice: `.top-tab` and `.top-user-trigger` both carry
   `margin-bottom: -1px` so their underlines sit ON the bar's hairline. Measured before
   the margin existed — the tabs' underlines ended at 45.8 and the trigger's at 50, a
   4.2px skew, at every width.

   The mechanism, measured rather than reasoned: the bar's height is the tallest child of
   `.top-actions`, which at baseline is a `.top-tab` (33.5px at 320, 34 at 375, 36 at
   414+). The trigger is normally SHORTER, because its content is the label's line box —
   13.5px at 320, 16px at 414+, since the font-size is `clamp()`ed — plus 18px of padding.
   An avatar taller than that line box makes the trigger the tallest child, and
   `.top-actions` is `align-items: center`, so everything else in the row lifts with it.

   A free avatar is therefore about 13px, which is too small to read as a face. So the
   circle is drawn at 22px and CONTRIBUTES 12px, which is inside the line box at every
   width. Swept in a real browser against the baseline heights (41.5 / 42 / 44 / 44 / 44):

     | size | block margin | bar at 320 / 375 / 414 / 768 / 1400 | underlines |
     |------|--------------|-------------------------------------|------------|
     | 13px | 0            | 41.5 / 42 / 44 / 44 / 44            | aligned    |
     | 14px | 0            | **42** / 42 / 44 / 44 / 44          | 0.2 skew   |
     | 22px | 0            | **50** / 50 / 50 / 50 / 50         | 4.2 skew   |
     | 18px | -2px         | **42** / 42 / 44 / 44 / 44         | 0.2 skew   |
     | 22px | -4px         | **42** / 42 / 44 / 44 / 44         | 0.2 skew   |
     | 22px | **-5px**     | 41.5 / 42 / 44 / 44 / 44            | aligned    |

   -5px is the smallest margin that holds at 320, which is the width that decides it: the
   tab is 33.5px there against 34px everywhere else, so 320 has half a pixel less headroom
   than any other width and every combination that failed, failed there first.

   **Do not remove this margin to "clean up", and do not change `width`/`height` without
   re-running that sweep.** The circle is verified to stay wholly inside the bar (top 13,
   bottom 35, in a bar of 0–41.5) and never to overlap the last tab. */
.top-avatar {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  /* See the measured table above. The circle paints at 22px and contributes 12px to the
     line box, which is what leaves the bar's height and the underlines untouched. */
  margin-top: -5px;
  margin-bottom: -5px;
  border-radius: 50%;
  overflow: hidden;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--border);
  color: var(--primary);
  /* Small, and NOT inheriting the trigger's `clamp()`: a letter in a 22px circle has a
     fixed amount of room, so a font-size that grew with the viewport would overflow the
     circle at wide widths while the circle stayed put. */
  font-size: 0.68rem;
  font-weight: 700;
  line-height: 1;
  /* The trigger is `text-align: center` by default as a button; the letter is centred by
     the flexbox above. `letter-spacing: 0` undoes `.top-brand`-style tracking if this ever
     moves under an element that sets it — a 0.02em kern on one glyph shifts it off centre. */
  letter-spacing: 0;
}

/* The photo fills the circle and is CROPPED to it rather than letterboxed: an avatar
   with bars down the sides looks like a mistake, and `PhotoLimits` already resized the
   source so the subject is centred. `object-fit: cover` is the difference. */
.top-avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* --- The trigger's cap comes down, because the row now carries a logo -------
   ## This is the SAME bug the `.top-user-trigger` rule above already documents
   That rule's measured table records `42vw` letting a 40-character email claim more of
   the row than was left, with the flexbox paying for it by SQUEEZING the wordmark's box
   so the glyphs are cut off inside it — `scrollWidth == clientWidth` still passes and the
   page just says "SafetySto". It fixed that with `min(42vw, 200px)`.

   The mark costs the row 26px of artwork plus an 8px gap = **34px**, and 42vw was already
   spending everything that was left. So the identical failure came back, measured with a
   Range around the wordmark's text node (the only thing that can see it), with a 40-char
   email as the label:

     | width | wordmark glyphs | its box | verdict  |
     |-------|-----------------|---------|----------|
     | 320   | 78.8px          | 68.9px  | CLIPPED  |
     | 375   | 90.7px          | 80.4px  | CLIPPED  |
     | 414   | 98.9px          | 91.6px  | CLIPPED  |
     | 768   | 108.5px         | 108.5px | fit      |
     | 1400  | 108.5px         | 108.5px | fit      |

   Swept for the largest cap that fits at all five widths, same method, same label:

     | cap               | 320     | 375     | 414     | label visible at 320 |
     |-------------------|---------|---------|---------|----------------------|
     | `min(42vw,200px)` | CLIPPED | CLIPPED | CLIPPED | 78.4px               |
     | `min(38vw,200px)` | CLIPPED | CLIPPED | CLIPPED | 71.3px               |
     | `min(34vw,200px)` | CLIPPED | fit     | fit     | 63.6px               |
     | `min(33vw,200px)` | fit     | fit     | fit     | 61.6px               |
     | **`min(32vw,…)`** | **fit** | **fit** | **fit** | **58.4px**           |

   **32 rather than 33**, deliberately: 33 is the exact edge (78.8 glyphs in a 78.8 box at
   320 — zero slack, the same knife-edge site/README.md warns about) and this row is
   measured in one browser's font metrics. 32 costs 3.2px of visible email at 320 and buys
   back real margin. The full value is still in the trigger's `title` and the label still
   ellipsises, so nothing is unreachable — a diver reads their whole address by hovering,
   or on any width from 768 up where the 200px arm governs and none of this applies.

   Deliberately a SEPARATE rule rather than an edit to the one above: that rule's comment
   is the record of the first time this happened, and its 42vw table stays true of a header
   with no logo in it. The override is what a logo cost. */
.top-user-trigger { max-width: min(32vw, 200px); }

/* --- ...and comes down again, because the row now carries a TOOLS CHEVRON ----
   Third time, same failure, same method. The full sweep, the 12px the chevron costs
   and why 29 rather than 30 are all in the "WHAT THE TOOLS CHEVRON COST" block up in
   the menu section — this is only the declaration, and it lives HERE because it must
   come AFTER the 32vw rule above to win the cascade (same selector, same specificity).

   Summary of the numbers up there: 320px carried 7.2px of slack before the chevron, 4.0
   and a clipped wordmark with the chevron unpaid, and 12.8px at 29vw — more than it
   started with. Costs 9.6px of visible email at 320 (58.4 → 48.8); the full address stays
   in the trigger's `title` and the label still ellipsises. From 768 up the `200px` arm
   governs, so this changes nothing there, and the 900px block below still raises it to
   320px. Do not merge this into the rule above: each one records what one addition to
   this row cost, and 32vw stays true of a header with no Tools chevron in it. */
.top-user-trigger { max-width: min(29vw, 200px); }

/* --- A dive card is now a button --------------------------------------------
   Phase 2 made every card open a detail page, so it has to LOOK tappable. The
   surface treatment itself is unchanged (`.dive-card` above is copied bytes and
   is not touched); this only adds the affordances.

   `:focus-visible` rather than `:focus` so a mouse click does not leave a ring
   behind, while keyboard users — who reach a card through the `tabIndex = 0`
   and the Enter/Space handler in index.html — still see where they are. */
.dive-card[role="button"] {
  cursor: pointer;
  transition: border-color 0.15s ease, background 0.15s ease;
}

.dive-card[role="button"]:hover {
  border-color: rgba(0, 242, 254, 0.35);
  background: rgba(255, 255, 255, 0.05);
}

.dive-card[role="button"]:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}

/* --- Getting the log out: the Import / Export tool (T-549, MOVED by T-554) ---
   *"downloading xslx and csv format on the dives....this would also mean i could check
   multiple dives and export the data to those formats...not just one"*, and then
   *"i think it might be best to have an import/export section of the page under tools just
   because we need to include importing formats too from the web."*

   ⚠️⚠️ **THE TICK-BOX RULES THAT MADE UP HALF OF THIS SECTION ARE DELETED.** `.dive-tick`,
   `.dive-list.selecting .dive-card` (and its `padding-left: 46px`), the box, the rotated
   `::after` checkmark, `.dive-card[role="checkbox"]` with its three affordance rules and
   `.dive-card.ticked` all went when choosing mode did: the picker is in a tool panel now and
   its rows are `<label>`s wrapping real `<input type="checkbox">`es, which need none of it.

   ⭐⭐ What that means for the note those rules carried, which was the important part and is
   still true: `.dive-card` IS NOT TOUCHED, AND IT CANNOT BE. Lines 493-498 are a VERBATIM copy
   of www/index.html's rule — the same reason `.dive-card-qr` was made a child rather than an
   edit (see that block's note, T-517). Choosing mode reached the card only through selectors
   that did not exist before, and removing them leaves the copied region exactly as it was; the
   `.dive-card[role="button"]` affordances fifteen lines up are once again the only ones a card
   has, because a card once again only ever does one thing. `tools/check-web-copy.sh` has
   nothing new to compare and nothing it compared has changed. */
.export-picker {
  /* A scroller, and the height is the whole point: a 300-dive log would otherwise push the
     three format buttons a full screen below the fold, so the diver who came here to press CSV
     would have to scroll past every dive they own to find it. 44vh keeps the buttons, the scope
     line and a dozen rows on screen together at the sizes this page is used at. */
  max-height: 44vh;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.03);
  /* Same reason as `.dive-ssi-bar`'s and `.ssi-card`'s: a flex or grid child holding this must
     be able to shrink below its content's natural width, or it sets the shell's floor. */
  min-width: 0;
}

/* ⭐ ONE ROW IS A `<label>`, so the hit target is the row and there is no click handler on it —
   the browser forwards the press to the box inside. That is what removes the ~100px dead zone
   the QR code punched in a `.dive-card` acting as a checkbox (T-549's `swallowUnlessChoosing`,
   reverted in T-554): a label has no such problem, because there is nothing inside it that
   stops an event.

   `grid` and not `flex`: the date and the site name must line up down the list, which is what
   makes a 130-row picker scannable, and a flex row would let each row size its own columns. */
.export-pick {
  display: grid;
  grid-template-columns: auto 5.5rem minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 10px;
  padding: 8px 12px;
  cursor: pointer;
  border-top: 1px solid var(--border);
}

/* The FIRST row has no rule above it: the container's own border is already there, and a second
   line one pixel inside it reads as a rendering fault. */
.export-pick:first-child { border-top: none; }

.export-pick:hover { background: rgba(255, 255, 255, 0.05); }

/* The tick state, said with the surface and not with the box alone — a checked box 16px wide is
   hard to scan for down a long list, and a tinted row is what a reader looking for "which four
   did I pick" actually sees. `:has()` rather than a class toggled from JS, so the style cannot
   disagree with the checkbox: there is one state and the browser owns it. */
.export-pick:has(.export-pick-box:checked) {
  background: rgba(0, 242, 254, 0.07);
}

/* The focus ring goes on the ROW, because the box is what receives focus and a 16px outline
   inside a full-width row is nearly invisible. `:focus-within` is the only way to say that. */
.export-pick:focus-within {
  outline: 2px solid var(--primary);
  outline-offset: -2px;
}

/* `accent-color` is the whole of the checkbox styling. A hand-drawn box would mean re-creating
   the indeterminate state, the disabled state and the focus ring, and losing the platform's own
   tick — which is the one control on this page a diver already knows how to read. */
.export-pick-box {
  accent-color: var(--primary);
  width: 16px;
  height: 16px;
  /* `margin: 0` because the UA stylesheet gives a checkbox 3px of its own, which would knock
     the four columns out of the alignment the grid exists to enforce. */
  margin: 0;
}

.export-pick-date {
  font-size: 0.78rem;
  color: var(--text-muted);
  /* Tabular figures so the dates form a column rather than a ragged edge. */
  font-variant-numeric: tabular-nums;
}

/* The site name is the only column allowed to be long, so it is the only one that truncates.
   `min-width: 0` in the grid template above is what lets it shrink far enough to do so. */
.export-pick-site {
  font-size: 0.85rem;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.export-pick-meta {
  font-size: 0.75rem;
  color: var(--text-muted);
  white-space: nowrap;
}

/* The empty log. Inside the picker's border rather than replacing it, so the panel does not
   change shape between "no dives" and "one dive". */
.export-empty {
  padding: 16px;
  font-size: 0.82rem;
  line-height: 1.5;
  color: var(--text-muted);
}

/* The two halves of the panel. `.transfer-section + .transfer-section` and not a margin on
   every one: a top margin on the first would push it off the header it belongs under. */
.transfer-section + .transfer-section {
  margin-top: 22px;
  padding-top: 18px;
  border-top: 1px solid var(--border);
}

.transfer-section { display: flex; flex-direction: column; gap: 10px; min-width: 0; }

/* `Import` and `Export`. Smaller than the panel's `<h2>` and louder than body text, which is
   the whole job — there are exactly two of these and they are what makes the panel read as one
   screen with two directions rather than two unrelated piles of buttons. */
.transfer-heading {
  margin: 0;
  font-size: 0.95rem;
  font-weight: 700;
  color: var(--text);
}

.transfer-note {
  margin: 0;
  font-size: 0.82rem;
  line-height: 1.5;
  color: var(--text-muted);
}

/* The running count. `--text` rather than `--text-muted`, matching `.dive-ssi-status`: it is
   the answer to "what will the next button press write", so it is the loudest line in the
   panel rather than a footnote under it. */
.export-scope {
  font-size: 0.82rem;
  line-height: 1.5;
  color: var(--text);
  overflow-wrap: anywhere;
}

/* ⚠️ `flex: 0 0 auto` — THE FIFTH INSTANCE, and the note above `.dive-ssi-bar` predicted it:
   "`.btn-secondary`'s `flex: 1` from the frozen region is wrong in every row but the
   `.form-actions` one it was written for, and `.logbook-header`, `.ssi-row-actions`,
   `.share-actions` and `.dive-map-offer` each carry their own `flex: 0 0 auto` for it. That was
   the FOURTH instance; the next new button row needs the same fix." This is that row. Without
   it, `Clear` and `Select all` appearing would stretch `Choose dives` to a third of the panel
   and the three buttons would resize every time the selection changed.

   `flex-wrap` because the format row holds `Excel (.xlsx)`, which is wide, and at 320px these
   must stack rather than push the panel past the viewport. */
.export-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.export-row .btn-secondary { flex: 0 0 auto; }

/* The two format buttons are the point of the panel, so they read as primary-ish without
   becoming a second `.btn-primary` gradient competing with the one in the header. */
.export-formats .btn-secondary {
  color: var(--text);
  border-color: rgba(0, 242, 254, 0.35);
}

.export-formats .btn-secondary:hover { border-color: var(--primary); }

.export-formats .btn-secondary:disabled {
  color: var(--text-muted);
  border-color: var(--border);
  cursor: default;
}

.export-status {
  font-size: 0.8rem;
  line-height: 1.5;
  color: var(--text-muted);
  overflow-wrap: anywhere;
}

/* The one message that is not good news — a build that failed, or text Excel could not hold.
   `--danger` and not a second red: the deletion banner and the form errors use the same one. */
.export-status.bad { color: var(--danger); }

/* --- One dive: the detail page ----------------------------------------------
   Reuses the card surface, the `--*` custom properties and `.key-title` from
   the copied region above. No new colours: a second palette on one screen is
   how the web app stops looking like the phone app. */
.detail-header {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 4px;
}

.detail-header h2 {
  font-size: 1.1rem;
  font-weight: 700;
  /* A long site name must wrap inside the row, not widen the page past the
     phone. `min-width: 0` is what lets this flex child shrink below its
     content; `overflow-wrap` handles a single unbroken 40-character word,
     which shrinking alone cannot break. */
  min-width: 0;
  overflow-wrap: anywhere;
}

.detail-back {
  flex: 0 0 auto;
  padding: 8px 12px;
  font-size: 0.8rem;
}

.detail-date {
  font-size: 0.75rem;
  color: var(--text-muted);
  margin-bottom: 16px;
}

.detail-block:not(:empty) { margin-bottom: 18px; }

/* Field rows: a two-column label/value grid on a phone. `minmax(0, 1fr)` on the
   value column rather than `1fr` — a bare `1fr` has an automatic minimum of the
   content's size, so one long note or coordinate string pushes the grid wider
   than the viewport and the whole page scrolls sideways. */
.detail-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 1px;
  background: var(--border);
  border: 1px solid var(--border);
  border-radius: 16px;
  overflow: hidden;
}

.detail-row {
  display: grid;
  grid-template-columns: 40% minmax(0, 1fr);
  gap: 12px;
  align-items: baseline;
  padding: 10px 14px;
  background: rgba(255, 255, 255, 0.03);
}

.detail-label {
  font-size: 0.75rem;
  color: var(--text-muted);
}

.detail-value {
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--text);
  /* Values are diver-entered free text — a buddy name or a suit description can
     be arbitrarily long, and one of them must not widen the page. */
  overflow-wrap: anywhere;
}

/* Same thresholds and same two classes the dive card's PPO₂ pill uses, so the
   card and the detail page cannot disagree about what counts as a warning. */
.detail-value.warn { color: var(--warning); }
.detail-value.bad  { color: var(--danger); }

/* A sentence, not a value. The SAC row's "Based on" line is the only user: it is
   prose several lines long, and at the value column's 600 weight it outweighed
   every actual measurement in the block — a caveat must not read as data. Kept
   at .78rem with a real line-height so a six-line fallback stays legible. */
.detail-value.note {
  font-size: 0.78rem;
  font-weight: 400;
  line-height: 1.45;
  color: var(--text-muted);
}
/* The starred bases (max depth, or no depth at all) are the cases where the
   figure above is not safe to plan gas from, so the sentence carries the same
   warning colour the PPO₂ row uses rather than sitting in muted grey. */
.detail-value.note.warn { color: var(--warning); }

/* Under 380px (iPhone SE and anything in split view) a 40% label column leaves
   too little for the value, so the row stacks. */
@media (max-width: 379px) {
  .detail-row {
    grid-template-columns: minmax(0, 1fr);
    gap: 2px;
  }
}

/* ## Notes in FULL — the point of the whole page
   No line clamp and no max-height, unlike `.dive-notes` on the list card.
   `pre-wrap` keeps the diver's own line breaks; the text is inserted with
   escapeHtml, so `pre-wrap` is a display rule and not a way to smuggle markup
   through. */
.detail-notes {
  font-size: 0.85rem;
  line-height: 1.6;
  color: var(--text);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  background: rgba(255, 255, 255, 0.03);
  border: 1px solid var(--border);
  border-radius: 16px;
  padding: 14px;
}

/* --- The SSI QR export ------------------------------------------------------
   Two surfaces share these rules: the block at the bottom of a dive's detail
   panel, and the `#/ssi` batch screen. Reuses the card surface, the `--*`
   custom properties and `.key-title` from the copied region above — no new
   colours, for the same reason the detail page introduced none.

   ## The QR code is the WIDEST FIXED-SIZE THING this app has ever rendered
   61x61 modules plus an 8-module quiet zone is 69 modules; at 8px each that is
   552px, against a 320px phone. site/README.md records the old layout being
   only 29px from the edge at 320, so there is no slack to spend here.

   Three independent guards, because a clipped QR code does not scan:
     1. `ssiQrSvg()` derives the module scale from the measured width and floors
        it at 2, so it never asks for more pixels than there are.
     2. `max-width: 100%` + `height: auto` below, so even a wrong measurement
        shrinks the code instead of overflowing the page.
     3. `svg { display: block }` — an inline SVG sits on a text baseline and
        contributes descender space, which is where a few stray pixels of
        vertical overflow come from. */
.ssi-card {
  background: rgba(255, 255, 255, 0.03);
  border: 1px solid var(--border);
  border-radius: 16px;
  padding: 14px;
  /* A flex/grid child holding this must be able to shrink below the code's
     natural width, or the QR pushes the column wider than the viewport. */
  min-width: 0;
}

.ssi-blurb {
  font-size: 0.8rem;
  line-height: 1.5;
  color: var(--text-muted);
  margin-bottom: 12px;
}

.ssi-qr {
  /* `text-align` centres the SVG (an inline-level box) without a flex context,
     which keeps this working while the code is being replaced in place. */
  text-align: center;
  margin: 0 0 10px;
}

.ssi-qr svg {
  display: block;
  margin: 0 auto;
  max-width: 100%;
  height: auto;
  border-radius: 10px;
  /* The white plate the encoder draws stops at the quiet zone; this pads it out
     so the code is not flush against a dark border, which some scanners read as
     part of the pattern. */
  padding: 8px;
  background: #fff;
  /* The image is generated locally from integer coordinates, but it is still a
     white rectangle on a dark page — the box-sizing keeps the padding inside the
     `max-width: 100%` so a 320px column cannot be overflowed by 16px of it. */
  box-sizing: border-box;
}

.ssi-note {
  font-size: 0.75rem;
  line-height: 1.5;
  color: var(--text-muted);
  margin-bottom: 6px;
}

.ssi-site-line {
  font-size: 0.78rem;
  line-height: 1.5;
  color: var(--text);
  /* An SSI site name is data from another host, rendered with `textContent`. It
     can be long and unbroken, and one of them must not widen the page. */
  overflow-wrap: anywhere;
  margin: 8px 0 0;
}

.ssi-count {
  font-size: 0.78rem;
  color: var(--text-muted);
  margin-bottom: 10px;
}

/* The raw payload, collapsed. Present so the shipped page can be compared
   against the unit test's golden strings without a debugger. */
.ssi-raw {
  margin-top: 10px;
  font-size: 0.72rem;
  color: var(--text-muted);
}

.ssi-raw summary { cursor: pointer; }

.ssi-raw code {
  display: block;
  margin-top: 6px;
  padding: 8px;
  background: rgba(255, 255, 255, 0.03);
  border: 1px solid var(--border);
  border-radius: 10px;
  font-size: 0.68rem;
  /* A 313-character string with no spaces in it. Both properties are needed:
     `anywhere` breaks it and `pre-wrap` is what stops the browser collapsing the
     structure a reader is checking. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  color: var(--text);
}

/* --- #/ssi rows ------------------------------------------------------------
   Collapsed by default and expanded one at a time. A marked row DIMS and STAYS
   PUT rather than moving or disappearing: a row vanishing under the finger is
   how a diver loses their place in 130 of them. */
.ssi-row {
  border: 1px solid var(--border);
  border-radius: 14px;
  margin-bottom: 8px;
  overflow: hidden;
}

.ssi-row-marked { opacity: 0.55; }

.ssi-row-head {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 3px;
  width: 100%;
  /* A real <button>, so Enter and Space work with no keydown shim — the same
     reasoning that made the header tabs buttons. It has to be reset to look like
     a row rather than a control. */
  background: rgba(255, 255, 255, 0.03);
  border: 0;
  color: var(--text);
  font: inherit;
  text-align: left;
  padding: 12px 14px;
  cursor: pointer;
}

.ssi-row-head:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: -2px;
}

.ssi-row-site {
  font-size: 0.88rem;
  font-weight: 600;
  /* A site name is diver-entered free text and may be one long word. */
  overflow-wrap: anywhere;
  min-width: 0;
}

.ssi-row-meta {
  font-size: 0.72rem;
  color: var(--text-muted);
}

.ssi-row-body { padding: 12px 14px 14px; }

.ssi-row-actions {
  display: flex;
  gap: 8px;
  margin-bottom: 12px;
}

/* `flex: 0 0 auto` so two short buttons stay their own size in a flex row, and
   `min-width: 0` so neither can force the row wider than the card. */
.ssi-row-actions .btn-secondary {
  flex: 0 0 auto;
  min-width: 0;
  padding: 7px 12px;
  font-size: 0.78rem;
}

/* --- THE DESKTOP LAYOUT ------------------------------------------------------
   ## ⚠️ READ THIS BEFORE CHANGING ANY NUMBER BELOW: the design target moved
   This file was written phone-first and stayed that way through two phases. It is
   not the target any more — "i dont care to make it iphone compatible anytime as
   its supposed to be shown from a computer....thats my thoughts". The web app is a
   DESKTOP app now. **Phone support is not dropped**: every guarantee below 900px
   is unchanged, and that is enforced by the fact that nothing here exists outside
   a `min-width` query. It is simply no longer what the layout is FOR.

   What stood here was one 900px block capping the column at 760px. On a 1440px
   monitor that is a 760px ribbon with 340px of dead space on each side, one dive
   per row, five dives on screen. The cap is now tiered and the dive list flows into
   columns.

   ## The tiers, and why these three numbers
   The alternative was full-bleed, and it is wrong for a reason that has nothing to
   do with taste: a detail page's field rows and a dive's notes are TEXT, and a line
   of text 1400px long is measurably harder to read — the eye loses the return
   sweep. So the shell grows in steps and stops.

     | viewport   | shell  | dive-list columns | card width |
     |------------|--------|-------------------|------------|
     | < 900      | 500px  | 1                 | 288–500    |
     | 900–1199   | 880px  | 2                 | ~432       |
     | 1200–1599  | 1120px | 3                 | ~361       |
     | ≥ 1600     | 1280px | 3                 | ~415       |

   **880 at the first tier** because a 900px viewport only has 868px inside
   `.app-content`'s 16px padding, so a bigger number would be a lie at the bottom of
   the tier — the shell would be fluid there and only reach its cap later, which is
   how a tier silently stops meaning anything.

   **Three columns start at 1200, not at 1024.** A third column has to be paid for
   out of the card, and the card is the thing being read: at a 1120px shell three
   cards are ~361px, which is WIDER than the 343px card a 375px phone gets, so
   nothing inside one is more cramped than it already is on a phone. Forcing three
   at 1024 would give ~318px cards — narrower than the 320px phone — and
   `.dive-stats` (a flex-wrap row of up to five stats) starts wrapping to three
   lines. Measured: at 1024 with two columns a card is 432px and every stats row is
   one line.

   **The shell STOPS at 1280 and the column count stops at 3.** A fourth column at
   1920 would be ~310px cards, i.e. worse than a phone, on the widest screen — the
   reductio of "use the width". And the detail page's two-up field rows are already
   ~620px each at 1280; past that the label sits an inch and a half from its value.
   So a 1920px monitor gets margins, deliberately, and that is the answer to "why
   isn't it full width".

   `--shell` exists so `.content-inner` and `.top-inner` CANNOT drift. They were two
   independent `max-width: 760px` declarations before; with three tiers that is six
   numbers to keep in step by hand, and the failure mode is the wordmark heading a
   column it is not the width of. Declared inside the query rather than on `:root`
   at the top of the file so that below 900px the property does not exist at all and
   the copied region's `max-width: 500px` is reached by nothing. */
@media (min-width: 900px) {
  :root { --shell: 880px; }

  .content-inner { max-width: var(--shell); }

  /* The header bar's inner column tracks `.content-inner` exactly, or the wordmark
     sits 130px inboard of the dive cards it is supposed to head. */
  .top-inner { max-width: var(--shell); }

  /* ## The dive list goes MULTI-COLUMN, and `.dive-card` is not touched to do it
     The card's own rule is at line 493, inside the copied region this file may not
     edit, and `diveCardHtml` in site/index.html is byte-copied from the frozen app.
     So the columns are done entirely on the CONTAINER: `.dive-list` is
     `display: flex; flex-direction: column; gap: 12px` up there and becomes a grid
     here. Not one byte of card markup or card CSS changes.

     ## GRID, not `columns: 2` — and the reason is the sort order, not the styling
     CSS multi-column would pack denser and needs one declaration. It also fills
     column one to the bottom BEFORE starting column two, so a list sorted
     newest-first reads down-then-across, and the newest dive's neighbour on screen
     is the middle of the log. Grid is row-major: visual order IS document order,
     which is also what keeps `renderDives`' click-by-INDEX wiring honest —
     `querySelectorAll` returns document order and index i is dives[i], so the third
     card in the top row is dives[2] and nothing about that is now a coincidence of
     layout. (Multi-column would ALSO break a card across a column boundary, which
     `break-inside` can suppress but only as a second fix for a self-inflicted
     problem.)

     `minmax(0, 1fr)` and not `1fr`: a bare `1fr` is `minmax(auto, 1fr)`, so a long
     unbroken site name or note would set the track's minimum and push the grid
     wider than the shell — the same trap `.detail-grid` and every flex child in this
     file records. `align-items: start` so a dive with three lines of notes does not
     inflate the two beside it into tall mostly-empty boxes. */
  .dive-list {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 16px;
    align-items: start;
  }

  /* Belt to `minmax(0, …)`'s braces, and an OVERRIDE of nothing: the frozen
     `.dive-card` rule sets no `min-width`, so this adds one rather than changing one.
     A grid item's automatic minimum is its content size; without this a single
     unbreakable string inside a card is enough to widen the track. */
  .dive-card { min-width: 0; }

  /* Field rows two-up. This predates the desktop layout and is unchanged: at any
     shell above 760px a one-per-line field list is a column of mostly-empty rows. */
  .detail-grid { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }

  /* ## The screens that must NOT take the width, and why each one is named
     The shell is sized for the LOGBOOK — a grid of cards and a detail page with a
     map and a graph in it. Three screens share that shell and are not that shape:

       * the two tool panels (nitrox, tank refill) and the Profile form. These are
         FORMS. `.form-row` is `display: flex` with `flex: 1 1 0` fields, so a
         1280px card gives a 600px-wide "O₂ %" input with its label an inch away —
         the width makes them worse, not better. 760px is the width they were
         measured and tuned at, so that is what they keep.
       * `#ssi-panel`, the batch export screen: a list of collapsed one-line rows.

     Scoped by ANCESTOR (`#tab-tools`, `#tab-profile`) and deliberately NOT written
     as a bare `.calculator-card` rule. `.login-card` is a `.calculator-card` too and
     its own `max-width: 400px` is declared EARLIER in this file, so a bare rule here
     would win on source order and blow the login card out to 760px on every desktop.
     (Checked, not assumed — that is the one collision this scoping exists to avoid.)

     `#detail-panel` is pointedly absent from this list: it is the screen the width
     was raised FOR. */
  #tab-tools .calculator-card,
  #tab-profile .calculator-card,
  #ssi-panel {
    max-width: 760px;
    margin-left: auto;
    margin-right: auto;
  }

  /* ## The one place the shell is still too wide, and it is a TEXT box
     A dive's notes are the diver's own prose, shown in full with no clamp (see
     `.detail-notes` above — that is the point of the detail page). Everything else on
     that page is a short field value in a grid cell, so the tiers alone are enough;
     a paragraph is not. `ch` rather than px because the constraint is a MEASURE — a
     count of characters per line, ~66–80 being the readable range — and `ch` is the
     unit that means that. At this file's 0.85rem it lands near 690px, so the box
     visibly narrows only where the shell is wider than that. */
  .detail-notes { max-width: 78ch; }

  /* The same argument for the share panel's two explanatory paragraphs, which are the
     longest prose on the site after the notes and are there to be READ before a
     button is pressed. */
  .share-note, .share-once { max-width: 78ch; }

  /* ⚠️ `.dive-map-box`'s desktop HEIGHT is deliberately NOT here — it is at the very
     bottom of this file. `.dive-map-box { height: 280px }` is declared ~300 lines
     BELOW this block, and a media query carries no specificity of its own, so a rule
     written here loses to it on source order. That is not a theory: it was written
     here first and MEASURED at 280px on a 1440px viewport. See the block at the end. */

  /* The 200px cap on the username trigger exists to stop a long email squeezing the
     wordmark inside the 500px column (see the measured table on the rule itself). The
     column is 880px or more here, so there IS room: 880 - 108.5 (wordmark) - 92 (two
     tabs) - 24 (padding) = ~655px available, and 320 spends well under half of it.
     Measured at 1400: wordmark glyphs 108.5 in a 108.5 box (no clipping), trigger
     305px, the whole 40-character email visible with no ellipsis at all. */
  .top-user-trigger { max-width: 320px; }
}

/* Tier 2 — three columns. The gap grows with the shell (16 → 18) so the columns read
   as separated at a card width where the eye has further to travel between them. */
@media (min-width: 1200px) {
  :root { --shell: 1120px; }

  .dive-list {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 18px;
  }
}

/* Tier 3 — the shell's ceiling. STILL THREE COLUMNS: see the table above for why a
   fourth is a worse card, not a better page. All this tier does is spend the extra
   240px on the cards and on the map/graph. */
@media (min-width: 1600px) {
  :root { --shell: 1280px; }
}

/* --- The Tools menu ---------------------------------------------------------
   The tool is chosen from the TOOLS DROPDOWN IN THE HEADER BAR. Its CSS is up in the
   header section, shared with the username menu ("i like the dropdown in the menu
   like Profile has for tools instead of where you select now....looks tacky").

   ## ⚠️ `.tool-picker` stood here and is DELETED, not commented out
   Four rules — `.tool-picker`, its `label`, its `select` and that select's `:focus` —
   styled a native `<select>` at the top of the Tools panel. The markup went with them;
   nothing on the page has that class any more, so keeping the CSS would be the third
   dead rule set in this file (see `.top-signout` and the bottom bar's four).

   The argument that rule set carried is worth keeping, because it is the argument
   AGAINST what is now shipping, and it was not wrong — it was outvoted. It said: a
   native `<select>` is block-level at `width: 100%`, so it is never a flex child
   competing for room in a squeezed row, and its popup is drawn by the OS outside the
   document, so an option's text has no width budget on this page at all. A header
   chevron has neither property, and the row's recorded failure mode is exactly that
   squeeze. That prediction came true on the first measurement: the wordmark clipped at
   320 and 375 the moment the chevron went in. What makes the trade acceptable is that
   the LIST is still `position: absolute` — so "Tank Refill Calculator" still has no
   width budget — and the 12px the chevron itself costs was swept and paid for with
   measurements (see "WHAT THE TOOLS CHEVRON COST" in the header section). What was
   spent, permanently, is 9.6px of visible email at 320px.

   The touch-target half of that argument (`ToolsView.swift`'s "the reason is the
   boat") is genuinely worse off: a 46px trigger in the header is a smaller target than
   a full-width select was, and the native app still uses a picker. If a diver in gloves
   struggles with this, that is the thing to revisit — not the styling. */

/* No `.tool-panel` rule exists, deliberately: the panels are shown and hidden with
   the `hidden` ATTRIBUTE, and `[hidden] { display: none !important }` at the top of
   this web-only section is what makes that work. Giving `.tool-panel` a `display`
   here is how that gets broken — it is the exact mistake the login/app shells and
   the detail panel already record. */

/* --- The safety notice ------------------------------------------------------
   Muted, small, centred, and ICONLESS on purpose. The wording is
   `SafetyNotice.text` from SafetyStopUI, byte for byte.

   A red banner with a warning triangle was the obvious styling and is the wrong
   one: this notice is on the screen EVERY time, unconditionally, so dressing it as
   an alert trains a diver to skip the region — and that region is where the
   calculator's REAL, conditional warnings appear ("DANGER: Exceeds contingency
   limit (1.6 ata)!"). The `.status-badge` above it owns the alarm vocabulary;
   this is standing text and looks like standing text.

   It must WRAP, never clip, so nothing here constrains its width or height. */
.safety-notice {
  margin-top: 20px;
  font-size: 0.7rem;
  line-height: 1.5;
  color: var(--text-muted);
  text-align: center;
}

/* --- The Tank Refill Calculator ---------------------------------------------
   Two grids, deliberately, and neither is a flex row.

   ## Why GRID and not the `.form-row` flex pattern the rest of the page uses
   `.form-row` is `display: flex; gap: 12px` with `.form-field { flex: 1 1 0 }`,
   and it is right for a row of independent fields. It is wrong for these two
   tables, because each has a NARROW LABEL COLUMN followed by columns that must
   line up ACROSS rows: "O₂ %", "He %", "N₂ %" and "PSI" are four separate flex
   rows under that pattern, each sizing its own label box, so the inputs drift out
   of alignment the moment one label is wider than another — and "N₂ %" is a
   different width from "PSI" in every font this page can get.

   A grid sizes each track ONCE for the whole table, so the columns cannot
   disagree. `minmax(0, 1fr)` rather than `1fr`: a bare `1fr` is `minmax(auto, 1fr)`
   and a native <input>'s intrinsic width then refuses to shrink below it, which is
   the overflow the dive form's `.form-field { min-width: 0 }` comment already
   records. The label track is `auto` so it takes exactly what the widest label
   needs and no more.

   The recorded failure mode on this page is not overflow, it is flexbox SQUEEZING
   a box while the browser CLIPS its glyphs and `scrollWidth == clientWidth` still
   passes (found three times in the header row). Fixed track counts with explicit
   zero minima are how that is avoided here rather than defended against. */
.refill-grid,
.refill-table {
  display: grid;
  align-items: center;
  column-gap: 8px;
  row-gap: 8px;
}

/* Label, START, FINISH. */
.refill-grid { grid-template-columns: auto minmax(0, 1fr) minmax(0, 1fr); }

/* Gas, ADD, TO, CU FT. */
.refill-table {
  grid-template-columns: auto minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr);
  row-gap: 6px;
}

/* The empty top-left cell. It exists so the header labels land over their own
   columns; `aria-hidden` in the markup keeps a screen reader from announcing a
   blank. */
.refill-corner { display: block; }

.refill-col-head {
  font-size: 0.65rem;
  font-weight: 600;
  letter-spacing: 0.055em;
  color: var(--text-muted);
  text-transform: uppercase;
}

.refill-row-label {
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--text-muted);
  white-space: nowrap;
}

/* Same box as `.form-field input` in the copied region above, value for value, so
   these match the login form's inputs rather than introducing a second control
   style. Written out rather than reusing the selector because these inputs are
   grid children, not `.form-field` children. */
.refill-input {
  width: 100%;
  min-width: 0;
  padding: 10px 12px;
  background: rgba(0, 0, 0, 0.3);
  border: 1px solid var(--border);
  border-radius: 10px;
  color: var(--text);
  font-size: 0.9rem;
  font-variant-numeric: tabular-nums;
}

.refill-input:focus {
  outline: none;
  border-color: var(--primary);
}

/* Derived N₂: muted and unboxed, so it reads as a computed value rather than
   something to tap. Matches `derivedValue` in TankRefillView.swift. */
.refill-derived {
  font-size: 0.85rem;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

.refill-gas {
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--text);
  white-space: nowrap;
}

.refill-cell {
  font-size: 0.85rem;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

/* The "To" column. Emphasised because it is the number a blender watches on the
   gauge — "add 346" needs arithmetic at the fill station, "top to 1346" is read
   straight off the dial. Colour AND weight, never colour alone. */
.refill-cell.strong {
  font-weight: 700;
  color: var(--primary);
}

.refill-summary-row {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  margin-top: 10px;
  font-size: 0.8rem;
  color: var(--text-muted);
}

.refill-summary-row b {
  color: var(--text);
  font-variant-numeric: tabular-nums;
  /* The mix string is short, but a long one must wrap rather than widen the card. */
  overflow-wrap: anywhere;
}

.refill-note {
  font-size: 0.75rem;
  line-height: 1.5;
  color: var(--text-muted);
  margin-top: 8px;
}

/* An unmakeable blend, and a mix that does not add up. Both carry the alarm
   vocabulary because both are CONDITIONAL — which is exactly why `.safety-notice`
   above does not (a notice shown every time, dressed as an alert, trains a diver
   to skip the region where these appear). */
.refill-problem {
  background: rgba(245, 158, 11, 0.12);
  border: 1px solid rgba(245, 158, 11, 0.35);
  border-radius: 12px;
  padding: 10px 12px;
  font-size: 0.8rem;
  line-height: 1.5;
  color: var(--warning);
}

.refill-warn {
  margin-top: 10px;
  font-size: 0.78rem;
  line-height: 1.5;
  color: var(--warning);
}

.refill-error {
  margin-top: 10px;
  font-size: 0.78rem;
  line-height: 1.5;
  color: var(--danger);
}

.refill-caveat {
  margin-top: 18px;
  font-size: 0.75rem;
  line-height: 1.55;
  color: var(--text-muted);
}

.refill-caveat p + p { margin-top: 6px; }

/* At 320px the two-input rows have about 100px per input once the label track and
   the gaps are paid for, which is enough for four digits at 0.9rem but leaves the
   12px side padding with no slack. Trimming the padding rather than the font size
   keeps the digits at full size — the header row's lesson is that shrinking TEXT is
   what ends in clipped glyphs. */
@media (max-width: 359px) {
  .refill-input { padding-left: 8px; padding-right: 8px; }
  .refill-grid,
  .refill-table { column-gap: 6px; }
}

/* --- The dive profile graph --------------------------------------------------
   A hand-rolled `<canvas>` depth curve on a dive's detail page, for a dive a
   COMPUTER logged. Reuses the card surface, the `--*` custom properties and
   `.key-title` from the copied region above — no new colours, for the same
   reason the detail page and the SSI export introduced none. The curve, the
   temperature trace and the pressure trace are drawn in `--primary`, `--accent`
   and `--safe`, taken as literals in the canvas calls because a 2D context
   cannot read a custom property.

   ## ⚠️ A canvas is the SECOND fixed-size thing this app renders, after the QR
   ## code, and it overflows the same way
   `site/README.md` records the header row being 29px from the edge at 320, and
   the QR code needed three independent guards. A canvas is worse in one respect:
   its bitmap size is set in JavaScript, so a measurement taken before layout
   settles bakes in a wrong width that no reflow corrects. Two guards, matching
   the QR code's:
     1. `paintProfile()` measures `.profile-canvases`' `clientWidth` and floors
        it at 160, so it never asks for more pixels than there are.
     2. `max-width: 100%` + `height: auto` below, so even a wrong measurement
        shrinks the canvas instead of widening the page.
   `display: block` for the third of the QR code's reasons: an inline canvas sits
   on a text baseline and contributes descender space, which is where a few
   pixels of vertical overflow come from. */
.profile-note {
  font-size: 0.75rem;
  color: var(--text-muted);
  margin-bottom: 8px;
}

.profile-card {
  background: rgba(255, 255, 255, 0.03);
  border: 1px solid var(--border);
  border-radius: 16px;
  padding: 10px;
  /* A flex/grid child holding this must be able to shrink below the canvas's
     natural width, or the graph pushes the column wider than the viewport. */
  min-width: 0;
}

/* The measured box. `paintProfile` reads this element's clientWidth, so its own
   width must come from the layout and never from its contents — hence the
   explicit `width: 100%` with `min-width: 0`: an intrinsic size here would be
   the canvas's, which is what is being decided. */
.profile-canvases {
  width: 100%;
  min-width: 0;
}

.profile-canvas {
  display: block;
  max-width: 100%;
  height: auto;
  /* A canvas is a rectangle of nothing until it is drawn on; without this a
     slow paint shows a hole in the card rather than an empty chart area. */
  background: rgba(0, 0, 0, 0.15);
  border-radius: 10px;
}

/* ## DELETED, and this rule caused a real visual bug before it went
   `.profile-canvas + .profile-canvas { margin-top: 8px; }` used to space the three stacked charts
   apart, back when they were bare sibling canvases. Each chart is now wrapped in a
   `.profile-plot`, and `.profile-plot + .profile-plot` below carries the same 8px — but this rule
   was left behind, and it then matched a pair it was never written for: the STATIC canvas and the
   OVERLAY canvas inside one wrapper.

   `margin` still applies to an absolutely-positioned box, so `top: 0` plus an inherited
   `margin-top: 8px` put the overlay **8 px below** the static layer. Every marker the overlay
   draws — the crosshair, the temperature and pressure dots, and the diver — was therefore painted
   8 px low while its arithmetic was exactly right, which is why the owner kept seeing them "below
   their lines" after a fix that had corrected the maths.

   Measured before the fix: `overlay.getBoundingClientRect().top - static...top === 8` on all three
   panels, `left` delta 0. It went unnoticed because a chart's own y-mapping is self-consistent —
   comparing marker pixels to curve pixels *within each canvas* reports ~0 px of error, and only
   comparing the two canvases' PAGE positions reveals it. `.profile-overlay` below now sets
   `margin: 0` explicitly so re-adding a sibling-margin rule cannot resurrect this. */

.profile-axis-note {
  margin-top: 6px;
  font-size: 0.7rem;
  line-height: 1.4;
  color: var(--text-muted);
  /* Says which way depth runs, which is the one thing about a dive profile a
     reader may not assume — and the axis numbers alone cannot say it. */
  overflow-wrap: anywhere;
}

.profile-summary { margin-top: 12px; }

/* The text fallback. A canvas is opaque to a screen reader, and the aria-label
   carries only max depth and duration; this is where the numbers live. Collapsed
   by default because it is long and the graph is the point. */
.profile-text {
  margin-top: 10px;
  font-size: 0.75rem;
  color: var(--text-muted);
}

.profile-text summary {
  cursor: pointer;
  color: var(--primary);
}

.profile-text ul {
  margin: 8px 0 0;
  padding-left: 18px;
  line-height: 1.6;
}

/* At 320px the canvas has ~276px of card once the page padding, the card border
   and its padding are paid for. Trimming the card padding rather than the canvas
   keeps the plot area — the header row's lesson applies to a chart as much as to
   text: shrink the frame, not the thing being read. */
@media (max-width: 359px) {
  .profile-card { padding: 8px; }
}

/* =============================================================================
   The editable Profile form (added 2026-08-13)
   -----------------------------------------------------------------------------
   The web app's first editable, saved form. Everything structural is REUSED from
   the copied region above — `.dive-form`, `.form-field`, `.form-row`,
   `.form-actions`, `.btn-primary`, `.key-title`, `.data-blurb`, `.data-count` —
   so not one copied byte moves for this feature. Only three things had no
   existing rule.

   ⚠️ `.profile-note` is DELIBERATELY NOT REDEFINED HERE. It already exists above
   (the dive-graph block's canvas caption: 0.75rem, muted, 8px bottom margin) and
   it is exactly what a field hint wants, so the form reuses it. A second
   `.profile-note` block would be the later of two rules for one class name and
   would silently retune the graph's caption — the kind of collision that is
   invisible until someone opens a dive. Only the `.bad` MODIFIER is new.
   ============================================================================= */

/* A complaint rather than a hint. Used for the birthday note, the SSI digits
   note, and the "your app has X" agency note — all three of which are things the
   diver needs to notice, unlike the standing hints beside them. */
.profile-note.bad { color: var(--danger); }

/* The save line under the form. Centred under a full-width button, because a
   left-aligned line under a centred control reads as belonging to whatever is
   below it. `aria-live="polite"` is on the element in the markup, not here. */
.profile-status {
  margin-top: 12px;
  font-size: 0.82rem;
  color: var(--text-muted);
  text-align: center;
}

.profile-status.bad { color: var(--danger); }

/* The `<input type="date">` BESIDE the birthday text field — see the markup for
   why the text field is the source of truth and this is additive (PROJECT_STATE
   5.76 and lesson 66: a date control cannot express "not set").

   `flex: 0 0 auto` with a fixed 44px: it must never take width from the field it
   sits beside. **This project's recorded failure mode for a flex row is not
   overflow — it is a squeezed neighbour while `scrollWidth == clientWidth` still
   passes** (the header wordmark, twice). So the calendar is pinned narrow and the
   text field keeps `flex: 1 1 0; min-width: 0`.

   The `.form-field input[type="date"]` rules in the copied region already give it
   `max-width: 100%`, `min-width: 0` and `appearance: none`; this only pins the
   box. Asymmetric padding because with `appearance: none` the indicator sits hard
   against the left edge otherwise.

   ⚠️ Safari on iOS renders this control as a wheel and sizes it differently. The
   `.value` contract (`YYYY-MM-DD`, no zone) is identical everywhere, so the 5.76
   safety does not depend on the widget — but the WIDTH does, and it must be
   measured on real Safari. If it cannot be made to fit at 320px, delete this
   control and the picker input: the text field is the source of truth and the
   feature works without it.

   ## ⚠️ THE SELECTOR IS THIS LONG BECAUSE A SHORT ONE LOST — MEASURED, NOT FEARED

   This was `.profile-calendar { width: 44px }`, and it **did not apply**. The
   copied region has `.form-field input { width: 100% }`, which is specificity
   (0,1,1) against a bare class's (0,1,0) — so the calendar took the FULL row
   width and squeezed the birthday text field to **26px**, measured in headless
   Chrome at 414, 768 and 1400.

   And it passed at 280, 320 and 375, which is the part worth remembering: the
   copied region also has `@media (max-width: 400px) { .form-row { flex-direction:
   column } }`, so below 400px the row STACKS and there is no neighbour to squeeze.
   **A phone-first check at 320 would have called this clean.** This project's
   recorded flex failure is a squeezed neighbour that still passes an overflow
   check; this is that bug, hiding above the phone widths instead of at them.

   `input[type="date"].profile-calendar` inside `.form-field` is (0,3,1), which
   beats `.form-field input` (0,1,1) and `.form-field input[type="date"]` (0,2,1)
   in the copied region without touching either. `flex-basis` is stated in the
   `flex` shorthand as well as `width`, so the box does not depend on which of the
   two the flex algorithm consults. */
.form-field input[type="date"].profile-calendar {
  flex: 0 0 44px;
  width: 44px;
  min-width: 0;
  padding: 10px 0 10px 8px;
}

/* ⭐ AND ITS TEXT IS HIDDEN, LEAVING THE ICON — T-183.
   ⚠️ A native `input[type="date"]` renders its OWN "mm/dd/yyyy" text, and inside the 44px box above that
   text is clipped to a fragment: the control read **"mi"** on screen, which is what the owner meant by
   *"it looks weird"*. The field is not there to be read — the text box beside it is — so the digits are
   hidden and only the calendar button remains.
   ⚠️ `color: transparent` AND hiding `::-webkit-datetime-edit`: the first covers engines without the
   pseudo-element, the second stops the hidden text still taking width and pushing the icon out of the box.
   ⚠️ The picker indicator is deliberately NOT hidden — it is the entire control. */
.form-field input[type="date"].profile-calendar {
  color: transparent;
}
.form-field input[type="date"].profile-calendar::-webkit-datetime-edit {
  display: none;
}

/* --- The dive-site map ------------------------------------------------------
   Leaflet's own stylesheet (`/leaflet-1.9.4.css`, vendored and sha256-pinned)
   is fetched by the SAME TAP that fetches the tiles, so none of these rules
   depend on it being present: everything below styles the box, the offer and
   the popup's contents, all of which exist before Leaflet does.

   ## THE FIXED HEIGHT IS THE WHOLE POINT — no layout shift on the tap

   `.dive-map-box` is 280px tall whether it holds the "Show map" offer or a live
   map. That is not decoration: without it the box grows on the tap, the notes
   and the SSI code below jump down the page, and a diver's finger lands on
   something that moved under it. Measured both states at 320/375/414/768/1400 —
   the box, and the top of the block after it, sit at identical offsets.

   A live Leaflet map ALSO requires a fixed height for a second, independent
   reason: `L.map()` reads `clientHeight` to decide how many tiles to request. In
   an auto-height box that is 0, and Leaflet then draws one row of tiles across
   an empty div — a map that is not slow but broken.

   ## Two guards against overflow at 320, the same pair the QR code uses
     1. `width: 100%` + `min-width: 0` here, so this can shrink below its
        content inside any flex or grid parent;
     2. `.leaflet-container { overflow: hidden }` in Leaflet's own sheet, so the
        tile plane cannot push the page sideways even mid-pan.
   Measured `scrollWidth == clientWidth` with the map open at all five widths. */
.dive-map-box {
  height: 280px;
  width: 100%;
  /* A flex/grid child holding this must be able to shrink below the tile
     plane's natural width — the same trap as the QR code and the dive form. */
  min-width: 0;
  border: 1px solid var(--border);
  border-radius: 16px;
  /* Clips the tile plane and the attribution to the rounded corners. Leaflet
     sets `position: relative` on this element itself when the map initialises,
     so the absolutely-positioned panes and controls anchor here. */
  overflow: hidden;
  background: rgba(255, 255, 255, 0.03);
}

/* The pre-tap state: the button and the sentence saying what tapping it sends,
   centred in the box the map will occupy. */
.dive-map-offer {
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 14px;
  text-align: center;
}

/* ⚠️ `flex: 0 0 auto` IS LOAD-BEARING, and it is the third time this exact trap has
   been hit in this file. The copied region gives `.btn-secondary` **`flex: 1`**,
   which is right in the horizontal `.form-actions` row it was written for and wrong
   in a flex COLUMN: the button grew to fill the 280px box and rendered as a tall
   rectangle with "Show map" floating in the middle of it. Caught by looking at a
   screenshot, not by any assertion — an overflow check and a "the button exists"
   check both passed the stretched version.

   `.logbook-header .btn-secondary` and `.ssi-row-actions .btn-secondary` above are
   the same fix for the same cause, so this follows the established pattern rather
   than inventing one. `align-self` is stated too, because in a column `align-items:
   center` handles the cross axis but a future `stretch` on the container would
   otherwise widen the button to the full box. */
.dive-map-offer .btn-secondary {
  flex: 0 0 auto;
  align-self: center;
}

/* The consequence line. `--text-muted` and 0.75rem match `.detail-label`, so it
   reads as the page's own quiet voice rather than as a scare notice — but it is
   never hidden behind a disclosure, because a consequence a diver has to open
   something to find is not disclosed. `max-width` keeps it to a readable
   measure on a tablet instead of one 700px line. */
.dive-map-note {
  font-size: 0.75rem;
  line-height: 1.5;
  color: var(--text-muted);
  max-width: 34rem;
}

.dive-map-failed {
  font-size: 0.8rem;
  color: var(--warning);
  padding: 10px 0 0;
  text-align: center;
}

/* `.dive-map-live` marks the box once a map is in it. The only thing it changes
   is the cursor affordance; the geometry is deliberately IDENTICAL to the
   placeholder's, which is what makes "no layout shift" true. */
.dive-map-live { cursor: grab; }

/* ⚠️ A KNOWN 1px ARTIFACT, MEASURED AND DELIBERATELY NOT "FIXED".
   At devicePixelRatio 1 a faint bright hairline can appear across the map where
   two tile rows meet. Diagnosed rather than guessed at:
     * it is NOT in the tile PNG — the tile's own bottom rows were read back
       through a canvas (blob: URL, so untainted) and are uniform;
     * the tile rows are geometrically CONTIGUOUS, gap exactly 0.000px;
     * but they meet on a HALF pixel, because the box's own offset is fractional.
       Each image is rasterised separately with 50% coverage of that shared device
       pixel, so the composite leaves ~25% of `.leaflet-container`'s own background
       showing. Proven by painting that background red and finding a red row at
       exactly that y.
   Measured amplitude above the surrounding rows: **+31 luminance at dpr 1, +6.9 at
   dpr 2, +7.2 at dpr 3.** Under ~12 does not read as a line, so on the phones this
   app targets it is not visible at all.
   The usual internet fix — sizing tiles to 256.5px — is REJECTED: it resamples every
   tile, which trades an invisible-on-phones hairline for permanently soft map text on
   the device that matters. Tinting the container background was tried and measured
   (31.2 -> 26.3), which is not a fix either, so it is not carried. */

.dive-map-hide {
  margin-top: 10px;
  font-size: 0.8rem;
  padding: 8px 12px;
}

/* Leaflet's popup renders on a WHITE bubble it draws itself (see
   `.leaflet-popup-content-wrapper` in its sheet), so this text must be dark —
   `var(--text)` is #f3f4f6 and would be invisible. This is the one place in
   this stylesheet where a literal colour is right rather than a custom
   property: the surface underneath it is the library's, not this app's. */
.dive-map-popup {
  font-size: 0.8rem;
  line-height: 1.45;
  color: #111827;
}
.dive-map-popup-site {
  font-weight: 600;
  /* A dive site is diver-entered free text and can be one 40-character word;
     the popup must not widen past the map it sits in. */
  overflow-wrap: anywhere;
}

/* Leaflet's controls are sized for a mouse. On a phone the zoom buttons and the
   attribution line both sit inside a 280px-tall box that also has to show a
   map, so the attribution is toned down to the size of the note above it. It is
   NOT hidden and its links are NOT removed: ODbL requires the credit, and this
   is the only reason `.leaflet-*` is referenced in this file at all. */
.leaflet-container .leaflet-control-attribution {
  font-size: 0.65rem;
  background: rgba(255, 255, 255, 0.85);
}

/* --- PUBLIC SHARE LINKS ------------------------------------------------------
   The block at the bottom of a dive's detail panel. Reuses the card surface, the
   `--*` custom properties and `.key-title` from the copied region above — no new
   colours and no new fonts, exactly like the detail page and the SSI export
   before it.

   ## Everything here is APPENDED below the marker, and that is not optional
   Lines 9-818 of this file are an extraction of www/index.html's `<style>` block.
   Editing them — even to add one correct rule — makes the two undiffable, and
   api/tests/run.php's app.css section now goes RED when they change: it md5s the
   copy delimited by its own content (the first `:root` block through the byte
   before T-183's docblock). ⚠️ The digest this docblock used to name,
   `sed -n '1,819p' … | md5 -q` == `749ff50a819e9954bac53f1e471e31e3`, was bound to
   two hand-picked line numbers and swept in this file's own header comment, so
   editing the DOCUMENTATION broke it — which is how T-517 found it.

   ## The one thing this block has to get right at 320px
   A share URL is `location.origin` plus `/api/share/` plus **64 hex characters**
   — around 100 characters with no space and no hyphen in it, which is the
   longest unbreakable string this app has ever rendered. PROJECT_STATE §62's
   lesson applies exactly: `min-width: 0` cannot help, because it lets the BOX
   shrink and the string then overflows the box it was allowed to shrink. Only
   breaking the characters keeps `scrollWidth == clientWidth`, so `.share-url`
   carries `overflow-wrap: anywhere` and that declaration is load-bearing rather
   than defensive. */
.share-card {
  background: rgba(255, 255, 255, 0.03);
  border: 1px solid var(--border);
  border-radius: 16px;
  padding: 14px;
  /* A grid/flex child holding a 100-character URL must be allowed to shrink
     below its content's natural width — the same reason `.ssi-card` has it. */
  min-width: 0;
}

/* The consequence, in the app's own quiet voice. `--text-muted` at 0.8rem
   matches `.ssi-blurb`, so it reads as explanation rather than as a warning
   notice — but it is never behind a disclosure, because a consequence a diver
   has to open something to find is not disclosed. Same rule as
   `.dive-map-note`. */
.share-note {
  font-size: 0.8rem;
  line-height: 1.5;
  color: var(--text-muted);
  margin-bottom: 12px;
}

/* The one-time sentence, and it is deliberately LOUDER than `.share-note`:
   `--text` rather than `--text-muted`, because a diver who skims it loses the
   link. Still not red — nothing has gone wrong, and a scare colour on a
   successful action teaches divers to ignore the colour. */
.share-once {
  font-size: 0.85rem;
  line-height: 1.5;
  color: var(--text);
  margin-bottom: 12px;
}

.share-form {
  display: flex;
  flex-direction: column;
  /* `align-items: flex-start` so the button is its own width in a column. Without
     it the copied region's `.btn-secondary { flex: 1 }` plus a stretch cross-axis
     renders a full-width slab — the exact trap `.dive-map-offer .btn-secondary`
     records having been hit three times in this file. */
  align-items: flex-start;
  gap: 12px;
}

/* The coordinate opt-in. A real `<label>` wrapping the checkbox, so the whole
   line is a tap target — a 13px box alone is below every touch-target guideline
   this app follows elsewhere. */
.share-check {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  font-size: 0.85rem;
  line-height: 1.45;
  color: var(--text);
  cursor: pointer;
  /* The label text is a full sentence and must wrap; the box must not shrink
     with it, so the flex child that CAN shrink is the text. */
  min-width: 0;
}

.share-check input {
  /* `flex: 0 0 auto` for the same reason as everywhere else in this file: a
     checkbox squeezed by a long label is a checkbox nobody can hit. `margin-top`
     optically centres a 16px box against the first line of a 0.85rem sentence
     rather than the middle of a three-line block. */
  flex: 0 0 auto;
  margin: 3px 0 0;
  width: 16px;
  height: 16px;
  /* The one place this app tints a native control. `accent-color` is honoured by
     every browser it ships to and needs no custom-drawn checkbox — which would
     mean an appearance:none rebuild and a keyboard-focus ring to re-implement. */
  accent-color: var(--primary);
}

.share-check input:disabled { cursor: default; }
.share-check input:disabled + span { color: var(--text-muted); }

/* ⚠️ THE LONGEST UNBREAKABLE STRING IN THE APP. See the section header: only
   breaking the characters keeps a 320px viewport from scrolling sideways.
   `user-select: all` so one tap selects the whole URL — the Copy button is the
   primary path, but `navigator.clipboard` is undefined on a non-secure origin
   and the manual route has to be workable. */
.share-url {
  overflow-wrap: anywhere;
  word-break: break-all;
  user-select: all;
  font-size: 0.78rem;
  line-height: 1.5;
  color: var(--text);
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 10px 12px;
  margin-bottom: 12px;
}

.share-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 12px;
}

/* `flex: 0 0 auto` + `min-width: 0`, following `.ssi-row-actions .btn-secondary`
   exactly rather than inventing a fourth variation of the same fix. */
.share-actions .btn-secondary,
.share-form .btn-secondary,
.share-row .btn-secondary {
  flex: 0 0 auto;
  min-width: 0;
  padding: 7px 12px;
  font-size: 0.78rem;
}

.share-list { margin-top: 14px; }

/* One existing link. `flex-wrap` so the meta line and the button stack on a
   narrow phone instead of the button being squeezed to nothing. */
.share-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 10px 0;
  border-top: 1px solid var(--border);
}

.share-row-meta {
  font-size: 0.78rem;
  color: var(--text-muted);
  /* The one flex child that may shrink, so a long meta line wraps rather than
     pushing the button off the card. */
  min-width: 0;
  flex: 1 1 auto;
}

/* `--warning` (#f59e0b), NOT `--danger` (#ef4444): a failed mint is a retry, not
   a data loss, and this app's red is reserved for a PPO₂ over 1.6 — a number a
   diver acts on. Both are declared in the copied region above; nothing new is
   introduced here. */
.share-error {
  font-size: 0.8rem;
  line-height: 1.5;
  color: var(--warning);
}

/* =============================================================================
   DESKTOP OVERRIDES THAT MUST BE LAST — source order, not specificity
   -----------------------------------------------------------------------------
   ## Why these two declarations are exiled to the bottom of the file
   Everything else the desktop layout does lives in the tiered block at line ~2044,
   next to the table explaining it, and that is where a reader should look first.
   These two cannot live there.

   **A media query adds NO specificity.** `@media (min-width: 900px) { .dive-map-box
   { height: 420px } }` and `.dive-map-box { height: 280px }` are both one class
   selector, so the later one in the file wins — and `.dive-map-box`'s own rule is
   ~300 lines BELOW the desktop block. Written up there it silently did nothing:
   measured 280px on a 1440px viewport, three tiers of layout applied around a map
   that had not grown at all. Same trap for `.profile-card`'s padding.

   The alternatives were worse. `!important` would beat source order but this file
   reserves it for one rule with a stated reason (`[hidden]`), and spending it on a
   height would make the next reader think there was a fight. Moving
   `.dive-map-box`'s base rule up is an edit to a block with a measured argument on
   it about layout shift. So: same cascade, later position, and a pointer left at
   both ends so neither can be found without the other.

   ## The map's desktop height, and why 280 was never a "safe default"
   280px was chosen against a ~340px-wide box on a phone — about 1.2:1, near square,
   which is the shape a map wants. The number stayed while the box grew:

     | viewport | box width | at height 280 | at the height below |
     |----------|-----------|---------------|---------------------|
     | 390      | 358px     | 1.28 : 1      | 1.28 : 1 (unchanged)|
     | 1024     | 858px     | 3.06 : 1      | 2.04 : 1  (420px)   |
     | 1440     | 1098px    | 3.92 : 1      | 2.29 : 1  (480px)   |
     | 1920     | 1258px    | 4.49 : 1      | 2.62 : 1  (480px)   |

   3.9:1 is not a map, it is a horizon strip: a dive site and its neighbours are
   spread over an area, and a letterbox shows one axis of it. The heights below are
   deliberately NOT enough to reach 1.2:1 — that would be an 1100px-tall map, taller
   than the laptop viewport it is on, and the coordinate row above it and the notes
   below it would both be off-screen. ~2:1 is the compromise, and it is stated as a
   compromise rather than presented as the right answer.

   ⚠️ **The height is still FIXED within any one width, which is the whole property
   `.dive-map-box`'s own comment is built on**: the "Show map" offer and the live map
   are the same height, so the tap causes no layout shift and the diver's finger does
   not land on something that moved. A media query changes the number; it does not
   make the height fluid. Leaflet reads `clientHeight` once at init, and crossing a
   breakpoint later is handled by its own `trackResize` (see `renderDetailMap` — this
   app deliberately binds no `invalidateSize` of its own).
   ============================================================================= */
@media (min-width: 900px) {
  .dive-map-box { height: 420px; }

  /* The phone trims this to 8px at 359px and under, for the stated reason that the
     plot area is what matters and the frame is what should shrink. On a desktop the
     reverse is affordable: 14px matches `.ssi-card` and `.detail-notes`, so the three
     boxes on a dive page have the same inset instead of the graph looking tighter than
     the text beside it. Costs the canvas 8px of a 1098px width — 0.7%. */
  .profile-card { padding: 14px; }
}

@media (min-width: 1200px) {
  .dive-map-box { height: 480px; }
}

/* --- The MySSI code on a dive CARD ------------------------------------------
   ## The codes moved onto the dive list, and the `#/ssi` screen is gone
   *"what if you added the QR code on the dive list page instead of the other 2 places"*.
   The `.ssi-*` rules above are UNTOUCHED and are NOT dead: `.ssi-card`, `.ssi-blurb`,
   `.ssi-qr`, `.ssi-note`, `.ssi-site-line` and `.ssi-raw` were shared by the deleted
   batch screen and the deleted detail block, so they no longer render — but they are
   left in place exactly as `.bottom-nav` and `.top-signout` were, because deleting them
   is a separate change with its own risk and this one is already wide.

   ## ⚠️ `.dive-card` IS NOT TOUCHED, and it cannot be
   Its rule is at line 493, inside the byte-for-byte extraction of www/index.html's
   `<style>` block that this file may not edit — md5-pinned by api/tests/run.php's
   app.css section, not by native/differential/differential.mjs, which pins
   www/index.html (T-517). So the code is a CHILD (`.dive-card-qr`,
   appended by `drawDiveCardCodes`) and everything below styles the child. Not one byte
   of card CSS changes, which is the same constraint the desktop grid worked under.

   ## The size, and why it is smaller than the detail page's code was
   `QR_MAX_PX` is 240 and `DIVE_CARD_QR_PX` is 160. A card is a SUMMARY — a site, a
   date, five stats, three pills, ~120px tall — and a 240px code inside one makes the
   image the card and the dive a caption. At 69 modules across (61 + an 8-module quiet
   zone, the widest code this feature produces) 160/69 floors to 2px per module, so the
   plate is 138px + 16px of padding = 154px. 2px per module is the floor `ssiQrSvg`
   already refused to go below, so the smaller box costs nothing in scannability.

   ## `margin-top` and a hairline, because a code is not a stat
   Everything else on a card is text in a flowing row. A white rectangle needs to read
   as an attachment to the card rather than as another field, so it gets the separator
   the detail page's blocks get. */
.dive-card-qr {
  /* `text-align` centres the SVG (an inline-level box) with no flex context, matching
     `.ssi-qr` above — which keeps this working while the code is REPLACED IN PLACE by
     the match pass. */
  text-align: center;
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
  /* A grid child holding a fixed-size image must be allowed to shrink below it, or the
     code sets the track's minimum and widens the whole 3-column grid past the shell.
     `.dive-card { min-width: 0 }` in the 900px query is the other half of this. */
  min-width: 0;
}

/* `display: block` because an inline SVG sits on a text baseline and contributes
   descender space, and `max-width: 100%` + `height: auto` so a code shrinks rather than
   overflowing if the measurement is ever wrong. Both copied from `.ssi-qr svg` above.

   ## ⚠️ `box-sizing: content-box`, and it is the OPPOSITE of `.ssi-qr svg` — MEASURED
   `.ssi-qr svg` carries `border-box`, and copying that here was a real defect caught by
   measuring px-per-module rather than by looking at the box. `ssiQrSvg` sets a `width`
   ATTRIBUTE of 138px (69 modules x 2px, its floor) and `border-box` makes that 138 the
   OUTSIDE of the plate — so the 16px of padding is taken out of the modules, leaving 122px
   for 69 of them: **1.768px per module, below the floor of 2 that function explicitly
   refuses to go under.** Measured in Chrome at 1440: a 138px plate where 154 was intended.
   `content-box` gives the modules their 138px and puts the padding outside, so the plate is
   154px and the module size is exactly the 2px the encoder asked for.

   That trade is safe here where it would not have been on `.ssi-qr`: the reason that rule
   chose `border-box` was that its width came from a MEASUREMENT of the container, so 16px
   of padding could overflow a 320px column the `max-width` had just fitted. This code's
   width is a CONSTANT 138px (`DIVE_CARD_QR_PX` is never measured — see `drawDiveCardQr`),
   and 154px fits the narrowest card this app has: 288px at a 320px viewport. Verified with
   the plates' own `getBoundingClientRect` at 320/375/414/768/1440 — zero escape the
   viewport, which is asserted rather than assumed because the Range walk that catches
   clipped TEXT cannot see an image at all. */
.dive-card-qr svg {
  display: block;
  margin: 0 auto;
  max-width: 100%;
  height: auto;
  border-radius: 10px;
  padding: 8px;
  background: #fff;
  box-sizing: content-box;
}

/* --- A long site name must BREAK, not escape the card ------------------------
   ## ⚠️ PRE-EXISTING DEFECT, found by this change's fixture and fixed here
   `.dive-site` is at line 507, inside the frozen copy of www/index.html's `<style>`
   block, and it sets `font-size`/`font-weight`/`color` and nothing else. A dive whose
   site name is one long unbroken word therefore lays out at its natural width and its
   glyphs leave the card: measured at 320px, a 62-character name occupied a 530px box
   inside a 254px row, running to x=563 in a 320px viewport.

   **`scrollWidth == clientWidth` PASSES while that happens**, which is exactly the trap
   PROJECT_STATE §62 records: `.app-content` is `overflow-x: auto`, so it absorbs the
   overflow and the document never scrolls — the text is simply cut off. Only the
   per-text-node `Range` measurement sees it, which is why that measurement exists.

   Proven pre-existing rather than caused by the codes: with every `.dive-card-qr`
   removed from the live page and the bar hidden, the same two text nodes report
   BYTE-IDENTICAL left/right values (33/563 and 563/626). The QR code is innocent; the
   fixture is just the first dive in this repo with a name long enough to show it.

   `overflow-wrap: anywhere` is the same one-line fix this file already applies to every
   other free-text field for the same reason — `.ssi-row-site` ("a site name is
   diver-entered free text and may be one long word"), `.ssi-site-line`, `.share-url` and
   `.detail-notes`. `anywhere` rather than `break-word` because only `anywhere` also
   shrinks the box's intrinsic minimum, which is what a flex/grid child needs.
   `min-width: 0` on the flex child, because `.dive-card-top` is a `space-between` row and
   a child's automatic minimum is its content size. */
.dive-site {
  overflow-wrap: anywhere;
  min-width: 0;
}

/* --- The one explanation, and the one report ------------------------------
   Said ONCE above the list instead of on all 130 cards. `hidden` until at least one
   code has drawn, so a diver who is not an SSI member sees neither this nor a code and
   the list is the screen it was before the feature existed.

   ## ⚠️ `.dive-ssi-actions` and `.dive-ssi-actions .btn-secondary` are DELETED (2026-08-15)
   They styled the `Match SSI dive sites` button, which is gone — the sites fill in
   automatically now (*"why cant we just automatically fill them in?"*). Deleted rather than
   left in place like the dead `.ssi-*` rules above: those were shared by two deleted screens
   and unpicking them was a second change, while these two selectors matched exactly ONE
   element in the whole site and it no longer exists. What they recorded is worth keeping,
   though, because it is the trap and not the button — `.btn-secondary`'s `flex: 1` from the
   frozen region is wrong in every row but the `.form-actions` one it was written for, and
   `.logbook-header`, `.ssi-row-actions`, `.share-actions` and `.dive-map-offer` each carry
   their own `flex: 0 0 auto` for it. That was the FOURTH instance; the next new button row
   needs the same fix. */
.dive-ssi-bar {
  background: rgba(255, 255, 255, 0.03);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 12px;
  margin-bottom: 14px;
  /* Same reason as `.ssi-card`'s: a flex/grid child holding this must be able to shrink
     below its content's natural width. */
  min-width: 0;
}

/* 0.8rem `--text-muted` matches `.ssi-blurb` and `.share-note`, so it reads as the
   page's own quiet explanation rather than as a warning notice. */
.dive-ssi-note {
  font-size: 0.8rem;
  line-height: 1.5;
  color: var(--text-muted);
}

/* The result line. `--text` rather than `--text-muted`, matching `.share-once`: it is the
   answer to what happened to the site field on every code above, so it is louder than the
   standing explanation above it. `overflow-wrap` because it is a sentence that grows with
   the count and must not widen the bar. `margin-top` was 8px against the button row that
   used to sit between it and the note; with the row gone it needs the note's own gap, so it
   is 6px — measured against `.dive-ssi-note`'s 1.5 line-height rather than left at a value
   that was spacing it from an element that no longer exists. */
.dive-ssi-status {
  font-size: 0.78rem;
  line-height: 1.5;
  color: var(--text);
  overflow-wrap: anywhere;
  margin-top: 6px;
}

/* =============================================================================
   The INTERACTIVE dive profile (added 2026-08-14)
   -----------------------------------------------------------------------------
   The crosshair read-out, the two-layer canvases the diver sprite rides on, and
   the zoom bar. Everything here is below the web-only marker, and it introduces
   no colour that is not already in the file.

   ## ⚠️ `.profile-canvas + .profile-canvas` HAD TO BECOME `.profile-plot +
   ## .profile-plot`, and missing it makes the three charts butt together
   Each canvas is now wrapped in a `.profile-plot`, so the adjacent-sibling
   selector that used to put 8px between them stops matching entirely — the
   canvases are no longer siblings of each other. The old rule is left in place
   (it is harmless and still correct for any unwrapped canvas) and the wrapper
   rule below is what actually spaces the charts now.
   ============================================================================= */

/* The positioning context for the two stacked canvases, and the element that
   carries every listener — the wrapper rather than the canvas, because it covers
   the axis gutters too and a scrub must not dead-zone in the left gutter. */
.profile-plot {
  position: relative;
  display: block;
  /* ## ⚠️ `pan-y`, NOT `none`, and this is the trap that decides whether the
     ## feature is usable on a phone
     `pan-y` keeps VERTICAL page scrolling with the browser — the direction a
     diver needs to read the rest of the page, past up to three charts that are
     220px tall each. It withholds horizontal panning and pinch-zoom, which is
     what lets a two-pointer gesture reach our handler at all. `touch-action:
     none` would also deliver the pinch but would take vertical scroll with it,
     trapping the page: the diver could not scroll past the chart.

     The consequence is handled in JS rather than here: with `pan-y` the browser
     may claim a gesture it judges vertical AFTER we have seen a pointerdown, and
     we then get `pointercancel`. That is the normal path, not an error. */
  touch-action: pan-y;
  /* A drag across a chart otherwise selects the surrounding text on desktop. */
  user-select: none;
  -webkit-user-select: none;
  border-radius: 10px;
}

/* Replaces `.profile-canvas + .profile-canvas` now that each canvas is wrapped.
   Same 8px, so nothing moves. */
/* ⚠️ DEAD since the three stacked profile plots became one: there is never a second .profile-plot
   for this to select. Kept as a comment rather than deleted silently because the rule it replaced
   carried an 8px argument that the note near line 2520 still refers to. */
/* .profile-plot + .profile-plot { margin-top: 8px; } */

/* The overlay sits exactly on top of the static layer. `pointer-events: none`
   so every event lands on the wrapper and there is one listener target rather
   than two that have to agree. */
.profile-overlay {
  position: absolute;
  top: 0;
  left: 0;
  /* ⚠️ Load-bearing, not tidiness. `margin` applies to an absolutely-positioned box, so ANY
     sibling-margin rule that matches this canvas silently offsets every marker it draws from the
     curve it is supposed to sit on. That is exactly what happened: a leftover
     `.profile-canvas + .profile-canvas { margin-top: 8px }` (see the note where it was deleted)
     put the whole overlay 8 px low, with correct arithmetic underneath. Stated here so the
     alignment does not depend on no other rule happening to match. */
  margin: 0;
  pointer-events: none;
  /* The static layer below already paints the card's placeholder background;
     a second opaque layer would hide the curve completely. */
  background: none;
}

/* The static layer keeps `.profile-canvas`' own rules and simply stops being
   positioned, so the overlay can be absolute against the wrapper. */
.profile-static { position: relative; }

/* ## The read-out, and why its height is FIXED
   Values appearing and disappearing must shift nothing: a page that reflows
   under the finger is how a diver taps the wrong thing (the `mapPlaceholder`
   lesson). So the row is a fixed height at every width, the idle sentence and
   the value cells occupy the SAME box, and they are swapped rather than
   stacked. `min-height` with centred content rather than a fixed `height`, so a
   forced text scale grows the box instead of clipping it. */
.profile-readout {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: 10px;
  min-height: 38px;
  margin-bottom: 8px;
  padding: 0 2px;
  /* Four cells at 320px is tight; this lets them shrink rather than wrap onto a
     second line, which would change the height. */
  overflow: hidden;
}

.profile-readout-cell {
  display: flex;
  flex-direction: column;
  /* A flex child holding text must be allowed to shrink below its content, or
     four cells push the row wider than the shell — the same `min-width: 0` the
     rest of this file needs for exactly this reason. */
  min-width: 0;
  flex: 1 1 0;
}

.profile-readout-label {
  font-size: 0.62rem;
  line-height: 1.2;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Tabular figures so the numbers do not JITTER as they change: proportional
   digits change width per digit, which makes a scrubbing read-out shimmer and
   is unpleasant to read at 60 Hz. */
.profile-readout-value {
  font-size: 0.86rem;
  line-height: 1.25;
  color: var(--text);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.profile-readout-idle {
  font-size: 0.72rem;
  line-height: 1.4;
  color: var(--text-muted);
}

/* ## The zoom bar, whose SPACE IS RESERVED whether or not it is showing
   Hidden with `visibility` and never with `hidden`, because appearing and
   disappearing would otherwise change the block's height — and doing that
   mid-pinch shifts the page under the finger that is pinching. The row occupies
   its height at all times. */
.profile-zoombar {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 34px;
  margin-top: 8px;
}

/* Real `<button>`s, so Enter/Space/Tab work with no shim. `flex: 0 0 auto` is
   the FIFTH instance of the trap this file records: the copied region gives
   `.btn-secondary` `flex: 1`, right in the `.form-actions` row it was written
   for and wrong in every other row — `.logbook-header`, `.ssi-row-actions`,
   `.share-actions` and `.dive-map-offer` all carry the same fix. These are not
   `.btn-secondary`, but they are a button row and they get it for the same
   reason. */
.profile-zoom-button {
  flex: 0 0 auto;
  min-width: 0;
  padding: 5px 10px;
  font-size: 0.75rem;
  color: var(--text);
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--border);
  border-radius: 8px;
  cursor: pointer;
}

.profile-zoom-button:hover { background: rgba(255, 255, 255, 0.1); }

.profile-zoom-label {
  font-size: 0.72rem;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ## PHOTOS on a dive — a grid, and it is no longer read-only
   ⚠️ A WRITTEN DECISION IS REVERSED HERE. This comment used to declare the grid read-only, on the
   grounds that a photo is only ever added or removed by the iOS app, *"because `Photos::delete`
   unlinks the bytes immediately and re-uploading the same id is refused forever afterwards."*

   ⚠️ The former sentence is DESCRIBED rather than quoted, and that is deliberate: its own words are
   the reversal's measurable receipt — a `git grep` for the old iOS-only clause across `site/` has to
   reach zero, and a verbatim quote here would keep it alive forever in the one file that is supposed
   to prove it is gone. The decision is on the record; its exact former wording is in git history.

   The `Photos::delete` half is still exactly true — it is why `.photo-confirm` below exists and why
   its copy is written out in full instead of handed to `window.confirm()`. What changed is the
   response to it: the consequence is now STATED in a two-step confirm rather than avoided by
   withholding the control. The premise that vanished is the other half, and it was never a property
   of the storage layer — only of which clients had been built.

   `auto-fill` with a `minmax` track rather than a fixed column count, so one photo is not
   stretched across the panel and four do not become unreadable at 320px. */
.photo-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: 8px;
  margin-top: 10px;
}

/* ## The tile is a BUTTON, stripped of every button style
   Not decoration: an `<img>` cannot take focus, so before this a keyboard could not reach a
   photograph and `closePhotoViewer`'s focus-restore was a no-op. The visible focus ring is the
   point of the exercise, so it is left to the browser rather than removed. */
.photo-tile {
  padding: 0;
  border: 0;
  background: none;
  display: block;
  width: 100%;
  border-radius: 8px;
  cursor: zoom-in;
  /* The grid track is the cell; without this a button's default `min-width: auto` can refuse to
     shrink below its content and push the last column out of a 320px screen. */
  min-width: 0;
}

.photo-tile:disabled {
  cursor: default;
}

/* ⚠️ `height: auto` with `aspect-ratio` rather than a fixed height: the renderer sets the `width`
   and `height` ATTRIBUTES from the manifest, so the browser reserves the correct box before the
   bytes arrive and nothing reflows as each photo lands. A fixed height here would override that
   and reintroduce the jump. `object-fit: cover` keeps a portrait and a landscape photo the same
   size in the grid without squashing either. */
.photo-thumb {
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: 8px;
  background: rgba(0, 0, 0, 0.25);
  display: block;      /* the cursor now lives on `.photo-tile`, the button that wraps this */
}

/* ## THE PHOTO VIEWER — one fixed stage, so the frame never moves
   Replaces `.photo-thumb.photo-zoomed`, which grew a tile in place. The owner's words on
   2026-08-16 were *"different size images show up"*: that rule set `aspect-ratio: auto`, so the
   zoomed tile took its height from the photograph and every step through a set of mixed portrait
   and landscape shots resized the page under the diver's thumb.

   ⚠️ **The stage has the aspect ratio, the image has `contain`.** The box is sized from the
   VIEWPORT and never from the photograph, which is the property that makes arrowing stable: a
   900x1200 portrait, a 1200x900 landscape and a 1600x900 panorama all letterbox into the same
   frame. Setting a height on the `<img>` instead would reintroduce exactly the jump being fixed. */
.photo-viewer-overlay {
  /* The image is the content, so the chrome gets out of its way — narrower padding than `.modal`
     and no blur behind, because a blurred backdrop under a photograph reads as a dirty lens. */
  padding: 12px;
  background: rgba(0, 0, 0, 0.82);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

.photo-viewer {
  width: 100%;
  max-width: 720px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

/* The counter (#photo-viewer-count) is bare text on the overlay, not a bordered surface —
   page text behind it cannot masquerade as the counter's own label because there is no
   button outline to frame it as UI chrome. The 82% scrim is sufficient for bare text. */
.photo-viewer-header { margin-bottom: 0; }

.photo-stage {
  position: relative;
  width: 100%;
  /* `4 / 3` matches the grid tile, so opening a photo is a continuation of the thumbnail rather
     than a different shape. `max-height` keeps the chrome reachable on a short landscape phone. */
  aspect-ratio: 4 / 3;
  max-height: 70vh;
  border-radius: 12px;
  background: rgba(0, 0, 0, 0.5);
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
}

.photo-stage img {
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
  display: block;
}

/* Hidden rather than removed while a photo is missing, so one element is reused across moves. */
.photo-stage img[hidden] { display: none; }

.photo-stage .photo-missing {
  position: absolute;
  inset: 0;
  background: transparent;
}

/* Two equal buttons, thumb-reachable at the bottom on a phone. `flex: 1` rather than a grid so a
   disabled end-of-set button still occupies its half and the pair never shifts sideways. */
.photo-viewer-nav {
  display: flex;
  gap: 8px;
}

/* ⚠️ OPAQUE — same fix as .top-user-menu (:1277). The overlay is rgba(0,0,0,0.82) with
   backdrop-filter:none, so any transparent button surface lets page text contribute pixels.
   The linear-gradient composites --card-bg over an opaque --bg base without introducing a
   colour. See the precedent comment at :1243-1260 for the full rationale. */
.photo-viewer-nav button {
  flex: 1;
  min-height: 44px;   /* the iOS touch-target floor; these are the only controls on this screen */
  background: linear-gradient(var(--card-bg), var(--card-bg)), var(--bg);
  border-color: rgba(255, 255, 255, 0.2);
}

/* Disabled affordance via colour, not element opacity — opacity would re-introduce the
   transparency bug by making the opaque surface 40% see-through again. */
.photo-viewer-nav button:disabled {
  background: linear-gradient(rgba(17, 24, 39, 0.9), rgba(17, 24, 39, 0.9)), var(--bg);
  border-color: rgba(255, 255, 255, 0.1);
  color: rgba(255, 255, 255, 0.35);
  cursor: default;
}

/* A row whose file is gone: said in place, because a broken-image glyph reads as the whole
   feature having failed rather than as one missing photograph. */
.photo-missing {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 90px;
  padding: 8px;
  border-radius: 8px;
  background: rgba(0, 0, 0, 0.25);
  color: var(--muted, #8b949e);
  font-size: 0.8rem;
  text-align: center;
}

.photo-note { margin-top: 8px; }

/* --- Photo WRITES: the cell wrapper, its action row, and the delete confirm ------
   ## Why there is a cell at all
   `.photo-tile` is a `<button>`, and a button cannot contain a button. So the grid track stopped
   being the tile and became this, with the tile, the action row and the confirm as siblings. */
.photo-cell {
  /* The same reason `.photo-tile` carries it, now one level further out: a grid item's default
     `min-width: auto` can refuse to shrink below its content and push the last column off a 320px
     screen. The track is `minmax(120px, 1fr)`, and `auto` beats the 120px minimum. */
  min-width: 0;
}

/* Wraps rather than squeezes. ⚠️ This project's recorded failure mode for a flex row holding
   controls is not sideways scroll — it is the row SQUEEZING its children while
   `scrollWidth == clientWidth` still passes, so three 12px labels in a ~170px grid track have to be
   allowed onto a second line instead of being compressed into unreadable slivers. */
.photo-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 6px;
}

/* `.btn-secondary`'s treatment written out rather than that class added to each control: the copied
   region gives `.btn-secondary` `flex: 1`, which in a wrapping row would force every control to an
   equal share and defeat the wrap above. `flex: 1 1 auto` lets a short "Rotate" stay short.
   `text-align: center` is what makes the `<label>` variant ("Replace") sit like the two `<button>`
   variants beside it — a label is not a button and does not centre its own text. */
.photo-act {
  flex: 1 1 auto;
  min-width: 0;
  padding: 6px 8px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: transparent;
  color: var(--text-muted);
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  text-align: center;
  cursor: pointer;
  /* The hidden file input inside the "Replace" label is absolutely positioned, so the label has to
     be its containing block or it would escape to the nearest positioned ancestor. */
  position: relative;
}

.photo-act:hover { color: var(--text); border-color: var(--primary); }

/* Two spellings of the same state, because two different elements carry it: a `<button>` can be
   `:disabled`, a `<label>` cannot, so the label gets a class and `aria-disabled` instead. */
.photo-act:disabled,
.photo-act.is-disabled {
  opacity: 0.5;
  cursor: default;
  color: var(--text-muted);
  border-color: var(--border);
}

/* The same red as `.dive-delete`, and for the same reason: the destructive control in a row of
   harmless ones must not look identical to them. It is a tint, not a fill — `.btn-danger`'s red
   fill is reserved for the second step, where the deletion actually happens. */
.photo-remove { color: #fca5a5; }

/* Block, not `inline-block`. MEASURED at 390px and 320px, against this stylesheet: as an
   inline-block the Add label lands on the SAME line as the show/hide toggle with a gap of exactly
   0px (390px: toggle 83→197, label 197→307; 320px: 162→272), because the renderer appends the two
   with no whitespace text node between them. Two bordered controls sharing one seam read as a single
   broken control. Neither variant overflowed at either width — so overflow is NOT the reason here,
   and nobody should "restore" `inline-block` on the grounds that it measured clean. Block also gives
   the at-cap sentence — "4 of 4 photos — remove one to add another" — a full line to wrap in. */
.photo-add {
  display: block;
  width: 100%;
  margin-top: 8px;
  text-align: center;
  cursor: pointer;
  /* Containing block for the visually-hidden input below. */
  position: relative;
}

.photo-add.is-disabled { opacity: 0.6; cursor: default; }

/* ⚠️ VISUALLY hidden, never `display: none`. A label forwards a click and an Enter press to its
   input only while the browser still considers that input rendered; `display: none` (or
   `hidden`) makes the whole Add and Replace surface unreachable by keyboard and, in some browsers,
   by the label click as well. 1px and transparent keeps it focusable and activatable while being
   invisible and taking no layout.
   ⚠️ MEASURED: a file input's own `scrollWidth` is ~180px inside this 1px box, so a sweep that reads
   `scrollWidth - clientWidth` on EVERY element will report ~+180 on each of these inputs. `overflow:
   hidden` is why that is contained: measured at 390px and 320px, no ancestor — cell, grid, block or
   body — gains a single pixel of scroll from it. Do not "fix" the input; fix the sweep. */
.photo-add input[type="file"],
.photo-act input[type="file"] {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
}

/* Set apart the way `.dive-danger` sets the dive-delete row apart: a destructive second step that
   sits flush against the controls it replaced reads as part of them. */
.photo-confirm {
  margin-top: 6px;
  padding: 8px;
  border: 1px solid rgba(239, 68, 68, 0.4);
  border-radius: 8px;
  min-width: 0;
}

/* Smaller and tighter than the modal's copy, because this one lives in a ~170px grid track rather
   than across a form. The WORDS are unchanged — the confirm's job is to be read. */
.photo-confirm .dive-confirm-text {
  font-size: 0.78rem;
  margin-bottom: 8px;
}

/* ⚠️ A COLUMN, overriding the copied region's row. `.form-actions` is reused rather than cloned, as
   intended, but `flex: 1` on "Keep it" and `.btn-danger`'s `flex: 1` on "Delete photo" inside a
   ~170px track is the squeeze this file records elsewhere: both labels would compress, and the
   one that must be read clearly is the destructive one. Stacked, each gets the full track. */
.photo-confirm .form-actions {
  flex-direction: column;
}

/* ## The Edit button in a dive's header
   `.detail-header` is already a `display: flex` row holding the back button and the title, and its
   `h2` carries `flex: 1` with `min-width: 0` so a long site name wraps rather than widening the
   page. So this needs only to refuse to shrink — `flex: 0 0 auto` — and to sit at the end.

   `margin-left: auto` is deliberately NOT used: the h2 already takes the slack, and adding an auto
   margin here would fight it and push the title off-centre on a short name. */
.detail-header .detail-edit {
  flex: 0 0 auto;
  align-self: center;
}

/* ## Deleting a dive — set apart, and the second step is not styled to be easy
   The row is separated by a rule rather than sitting in `.form-actions`, so Delete is never adjacent
   to Save. A destructive control beside the one a diver reaches for constantly is how a mis-tap
   becomes a lost dive. */
.dive-danger {
  margin-top: 18px;
  padding-top: 14px;
  border-top: 1px solid rgba(255, 255, 255, 0.08);
}

.dive-delete { color: #fca5a5; }

.dive-confirm-text {
  font-size: 0.85rem;
  line-height: 1.45;
  color: var(--muted, #8b949e);
  margin-bottom: 10px;
}

/* ⚠️ The confirm button is the ONLY red fill on this site, and that is the point: nothing else looks
   like it, so it cannot be mistaken for Save. `.btn-danger` did not exist before this. */
.btn-danger {
  background: #b91c1c;
  border: 1px solid #ef4444;
  color: #fff;
  padding: 12px;
  border-radius: 8px;
  font: inherit;
  font-weight: 600;
  flex: 1;
}

.btn-danger:disabled { opacity: 0.6; }

/* --- The PENDING-DELETION banner, and the account-delete confirm ---------------
   ## Why the banner is a COLUMN and not a text-plus-button row
   ⚠️ This project's recorded failure mode for a flex row holding a native control is not
   sideways scroll — it is the row SQUEEZING its text child while `scrollWidth ==
   clientWidth` still passes (see `.top-brand` and the 359px query above). "This account is
   scheduled for deletion on or after 2026-11-18…" is ~180 characters, and "Cancel deletion"
   is a 118px button that must not shrink; side by side at 320px the sentence gets a ~14px
   column. So the button sits UNDER the text at every width, which needs no budget at all.

   Danger-tinted rather than the `.read-only-note` blue: it is the same box shape and the
   same padding, so it reads as part of this page's vocabulary, but nothing else on the site
   is this colour except `.login-error` and `.btn-danger` — both of which are also things
   that went wrong or are about to. `margin-bottom` matches `.read-only-note` so the first
   panel below sits where it always does.

   `position: static` deliberately: it is the first child of `.content-inner`, INSIDE the
   scroller, so it scrolls away with the page. Making it sticky would put a second fixed band
   under the sticky header and take ~90px off an iPhone's usable height on every screen. The
   diver reaches it by scrolling up, which is where they already are on arrival, and both
   handlers that raise it call `setAppScrollTop(0)`. */
.deletion-banner {
  background: rgba(239, 68, 68, 0.12);
  border: 1px solid rgba(239, 68, 68, 0.4);
  border-radius: 12px;
  padding: 12px;
  margin-bottom: 14px;
}

.deletion-banner-text {
  font-size: 0.82rem;
  line-height: 1.5;
  color: #fca5a5;
  /* The date is interpolated with escapeHtml; wrapping stops a long sentence — or an
     unexpectedly long value — from widening the card. Same rule as `.login-error`. */
  overflow-wrap: anywhere;
}

.deletion-banner-text strong { color: #fecaca; }

/* `margin-top` rather than a gap on the parent: the status line below is `hidden` most of
   the time, and a parent gap would reserve space for it. */
.deletion-banner-actions {
  display: flex;
  margin-top: 10px;
}

/* Full width, because it is the only control in the band and the one thing a diver having
   second thoughts is looking for. `.btn-secondary` is already `flex: 1`; this is the parent
   that lets it take the width. */
.deletion-banner-actions .btn-secondary { flex: 1; }

/* The consequences list inside the account-delete confirm. `.dive-confirm-text` styles the
   block (size, line-height, muted colour) and is shared with the dive delete, which has no
   list — so these two rules add the list without touching that block's shape.

   `padding-left: 18px` rather than `list-style: none`: a diver has to be able to see that
   this is a list of separate consequences, and the marker is what does that. 18px is the
   smallest indent that keeps a wrapped second line clear of the bullet at 320px. */
.dive-confirm-text p { margin: 0 0 6px; }
.dive-confirm-text p + .account-confirm-list { margin-top: 0; }

.account-confirm-list {
  margin: 0 0 12px;
  padding-left: 18px;
}

.account-confirm-list li { margin-bottom: 5px; }

/* The password field inside the confirm. `.form-field` is `flex: 1 1 0` for use inside a
   `.form-row`; here it is a lone child, so it needs its own top gap and nothing else. */
#account-delete-confirm .form-field { margin-bottom: 4px; }

/* ============================================================================
   ⭐⭐ GET THE APP — the App Store lockup on the login card (T-185)
   ============================================================================
   SafetyStop 1.0 reached the App Store on 2026-08-29. The login card used to state that accounts are
   created in the iOS app and offer no way to get there; this is the way there.

   The owner asked for the Apple badge INSIDE the code rather than a headline and a sentence beside it:
   *"add that apple available on app store logo to it instead of..."* and *"try to keep in line with the
   color scheme"*.

   ## ⚠️⚠️ THE TWO THINGS HERE THAT DECIDE WHETHER IT SCANS AT ALL

   1. **The badge destroys the modules underneath it.** Nothing about the overlay is decorative — the
      symbol reads only because Reed–Solomon reconstructs what is missing, which is why the code is encoded
      at ECC HIGH. The overlay is kept to a fraction of that budget and `tools/appstore-qr-check.mjs`
      decodes a real screenshot of this element to prove it.
   2. **Cyan is not available.** `--primary: #00f2fe` is the brand colour and it is a LIGHT one: cyan
      modules on white have almost no luminance contrast, and a scanner distinguishes dark from light. So
      "in line with the colour scheme" resolves to the DARK navy end of the same palette — `#16233c` for
      the data modules, `--bg`'s own `#0b0f19` for the finder patterns. Contrast against white is asserted
      by the check, not eyeballed.

   ⚠️ EVERYTHING HERE IS BELOW THE "web-only additions" MARKER, because everything above it except
   T-183's desktop block is byte-for-byte the native app's own stylesheet, md5-pinned by
   `api/tests/run.php`'s app.css section (T-517 — `native/differential/differential.mjs` pins
   `www/index.html`, not this file). */
.app-cta {
  display: flex;
  justify-content: center;
  margin: 18px 0 6px;
  text-decoration: none;
}

.app-cta-lockup {
  position: relative;
  display: block;
  /* ⚠️ 208px is not arbitrary. At ECC HIGH the symbol is 45 modules across, so 208px leaves ~4.2px per
     module after the quiet zone — comfortably above the ~3px where phone cameras start to struggle. The
     MEDIUM version was 37 modules and fitted in 116px; buying redundancy costs area, and this is the bill. */
  width: 208px;
}

/* The white plate. A QR is a contrast pattern and scanners look for dark modules on light, so this stays
   white in every theme — the symbol is never inverted for dark mode. */
.app-cta-code {
  display: block;
  background: #fff;
  border-radius: 12px;
  padding: 8px;
  box-sizing: border-box;
  line-height: 0;
}
.app-cta-code svg { display: block; width: 100%; height: auto; }

/* ⭐ THE BADGE, centred over the code.
   ⚠️ The white ring (`box-shadow` spread, not a border) is the clear space Apple's guidelines require —
   and it does a second job here: it separates their black artwork from the navy modules behind it, so the
   badge reads as an object on top rather than as part of the pattern. */
.app-cta-badge {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 96px;
  height: auto;
  border-radius: 5px;
  box-shadow: 0 0 0 5px #fff;
}

/* ⚠️⚠️ ON A PHONE THE CODE GOES AND THE BADGE STAYS, at its own size.
   You cannot scan the screen you are holding — but unlike the previous version of this card, the badge is
   now the ONLY call to action, so hiding the lockup wholesale would leave a visitor with nothing. The
   badge un-anchors itself and becomes the button it is everywhere else. */
@media (max-width: 599px) {
  .app-cta-lockup { width: auto; }
  .app-cta-code { display: none; }
  .app-cta-badge {
    position: static;
    transform: none;
    width: 140px;
    box-shadow: none;
    border-radius: 0;
  }
}

/* ============================================================================
   ⭐⭐ THE LANDING PAGE (T-501)
   ============================================================================
   The owner's report was the whole brief: *"i ddont see that stuff on the website....all i see is the
   login when i goto https://safetystop.cloud/"*. `/` was a sign-in box on an empty page — for a
   visitor who has never heard of SafetyStop, and for a crawler, it said nothing at all.

   ## ⚠️ WHY THIS IS `.login-shell.landing-shell` AND NOT A NEW ELEMENT
   `#login-screen` is what `showLogin`/`showApp` toggle and what a dozen browser harnesses in `tools/`
   drive (`#login-screen form`, `#login-screen input`, `#login-email`, `#login-form`). The landing page
   is therefore built INSIDE that element rather than beside it: no new route, no `.htaccess` change,
   and every existing sign-in path keeps working unchanged.

   ## ⚠️⚠️ `.login-shell` ITSELF IS SHARED — DO NOT TOUCH IT
   `site/admin.html`, `admin-api.html`, `admin-feedback.html`, `admin-options.html`,
   `admin-analytics.html` and `admin-codes.html` all build their login card into a `.login-shell`, and
   they get the centred one-card flexbox that class exists for. That is why every rule below is scoped
   to the extra `.landing-shell` class: the six admin consoles must be unaffected by all of it.

   ## ⚠️ EVERYTHING IS EXPLICIT BECAUSE THE RESET IS TOTAL
   The frozen region above starts with `* { margin: 0; padding: 0 }`, so there is no UA margin on any
   heading or paragraph here and no list bullet indent. A missing `margin` reads as a layout bug rather
   than as tight spacing, so every gap on this page is written down.

   ## ⚠️ ANCHORS ONLY, NEVER A `<button>`, IN ANY OF THIS MARKUP
   `tools/desktop-layout-check.mjs` (`TOO_WIDE = 320`) visits `/` and the marketing pages and fails a
   button wider than 320px. Landing calls to action are links, which is also what they are.

   Below the "web-only additions" marker, like everything else here — above it is a copy of the native
   app's stylesheet, md5-pinned by `api/tests/run.php`'s app.css section (T-517). --------- */

/* ⭐ THE NO-FLASH RULE. The pre-paint script in index.html's `<head>` sets `data-ss-returning` when
   this browser has reached the app before, and this hides the landing for that paint — so a signed-in
   diver goes straight to their log instead of watching marketing appear and vanish. It beats
   `.login-shell.landing-shell { display: block }` on the id, so no `!important` is needed; `showLogin`
   removes the attribute the moment we know the session is gone. A first-time visitor, a signed-out
   diver and a crawler never have the attribute and see the page in the initial HTML. */
html[data-ss-returning] #login-screen { display: none; }

.login-shell.landing-shell {
  /* Block flow, not the shared centred flex: this element is now a whole scrolling page. */
  display: block;
  align-items: initial;
  justify-content: initial;
  padding: 0;
}

/* --- The bar across the top -------------------------------------------------
   ⚠️ DELIBERATELY NOT STICKY. A sticky bar covers whatever a script scrolls to the top of the
   viewport, and the browser harnesses in `tools/` click `#login-email` and `#login-submit` at their
   centre after scrolling them into view — the classic "element is covered by a fixed header" failure.
   A landing page short on chrome does not need it, and a green sign-in harness is worth more. */
.landing-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  max-width: 1100px;
  margin: 0 auto;
  padding: max(14px, env(safe-area-inset-top)) max(16px, env(safe-area-inset-right)) 14px
           max(16px, env(safe-area-inset-left));
  border-bottom: 1px solid var(--border);
}

.landing-mark {
  font-size: 1.15rem;
  font-weight: 800;
  letter-spacing: 0.02em;
  text-decoration: none;
  background: linear-gradient(135deg, var(--primary), var(--accent));
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
}

/* On a narrow phone the menu is a scrolling row rather than a wrapped block: eight items stacked two
   deep would push the headline off the screen, which is the one thing this page exists to show. */
.landing-menu {
  display: flex;
  align-items: center;
  gap: 6px;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  /* Scrollbar gutter would clip the focus ring on the last link. */
  padding-bottom: 2px;
}

.landing-menu a {
  color: var(--text-muted);
  text-decoration: none;
  font-size: 0.82rem;
  padding: 7px 10px;
  border-radius: 999px;
  white-space: nowrap;
}

.landing-menu a:hover { color: var(--text); background: rgba(255, 255, 255, 0.06); }

.landing-menu a.landing-menu-cta {
  color: var(--bg);
  font-weight: 700;
  background: linear-gradient(135deg, var(--primary), var(--accent));
}

.landing-menu a.landing-menu-cta:hover { color: var(--bg); }

.landing {
  max-width: 1100px;
  margin: 0 auto;
  padding: 0 max(16px, env(safe-area-inset-right)) 48px max(16px, env(safe-area-inset-left));
}

/* --- Hero ------------------------------------------------------------------- */
.landing-hero {
  display: flex;
  flex-direction: column;
  gap: 28px;
  padding: 28px 0 8px;
}

.landing-pitch .brand-header { margin-bottom: 14px; }

.landing-headline {
  font-size: 1.75rem;
  font-weight: 800;
  line-height: 1.2;
  letter-spacing: -0.01em;
  margin-bottom: 12px;
}

.landing-lead {
  color: var(--text-muted);
  font-size: 1rem;
  line-height: 1.6;
  margin-bottom: 18px;
}

.landing-points {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 12px;
  margin-bottom: 18px;
}

/* A drawn marker rather than a bullet: `list-style` is unavailable in a flex container, and the reset
   above removed the indent a bullet would need anyway. */
.landing-points li {
  position: relative;
  padding-left: 22px;
  color: var(--text-muted);
  font-size: 0.92rem;
  line-height: 1.55;
}

.landing-points li::before {
  content: "";
  position: absolute;
  left: 4px;
  top: 0.55em;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--primary);
  box-shadow: 0 0 8px var(--primary-glow);
}

.landing-points strong { color: var(--text); font-weight: 700; }

.landing-jump { font-size: 0.9rem; }
.landing-jump a { color: var(--primary); text-decoration: none; font-weight: 600; }
.landing-jump a:hover { text-decoration: underline; }

/* The sign-in card's own heading. It exists because the brand lockup MOVED out of this card and into
   the hero beside it — `.brand-header h1` is styled in the frozen region above, so the gradient
   wordmark travels with its own markup rather than being re-implemented here as a near-copy that
   would drift from the native app's. */
.login-title {
  font-size: 1.15rem;
  font-weight: 700;
  text-align: center;
  margin-bottom: 6px;
}

/* --- The sections down the page --------------------------------------------- */
.landing-section { padding: 40px 0 0; }

.landing-section > h2 {
  font-size: 1.35rem;
  font-weight: 800;
  letter-spacing: -0.01em;
  margin-bottom: 10px;
}

.landing-section > p {
  color: var(--text-muted);
  font-size: 0.93rem;
  line-height: 1.6;
  margin-bottom: 12px;
  max-width: 46rem;
}

/* ⚠️ Both selectors repeat `.landing-section >` on purpose. `.landing-section > p` above sets the muted
   colour and the body size for every paragraph in a section, and a bare `.landing-section-lead` (0,1,0)
   loses to it (0,1,1) — the rule would appear to do nothing. Matching its shape wins on specificity
   without an `!important` that the next rule would then have to fight. */
.landing-section > p.landing-section-lead { color: var(--text); }

.landing-section > p.landing-note { font-size: 0.8rem; }

.landing-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 16px;
  margin-top: 4px;
}

.landing-card {
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 20px;
  padding: 20px;
}

.landing-card h3 {
  font-size: 1rem;
  font-weight: 700;
  color: var(--primary);
  margin-bottom: 8px;
}

.landing-card p {
  color: var(--text-muted);
  font-size: 0.9rem;
  line-height: 1.6;
}

.landing-card p + p { margin-top: 10px; }
.landing-card strong { color: var(--text); font-weight: 700; }

/* --- Screenshots ------------------------------------------------------------
   A scrolling strip on a phone, a grid on a wide screen. The captures are 600x1298 and both
   dimensions are on every `<img>`, so the strip reserves its height before the lazy images land and
   nothing below it jumps. */
.shot-strip {
  display: flex;
  gap: 16px;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scroll-snap-type: x proximity;
  padding: 4px 0 10px;
}

.shot {
  flex: 0 0 232px;
  scroll-snap-align: start;
}

.shot img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 18px;
  border: 1px solid var(--border);
  background: var(--card-bg);
}

.shot figcaption {
  color: var(--text-muted);
  font-size: 0.8rem;
  line-height: 1.5;
  margin-top: 10px;
}

.shot figcaption strong { color: var(--text); font-weight: 700; }

.landing-close { padding-top: 44px; }

.landing-footer {
  margin-top: 44px;
  padding-top: 20px;
  padding-bottom: calc(16px + env(safe-area-inset-bottom));
  border-top: 1px solid var(--border);
  text-align: center;
}

.landing-footer-links {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 8px 18px;
  margin-bottom: 10px;
}

.landing-footer-links a {
  color: var(--primary);
  text-decoration: none;
  font-size: 0.85rem;
}

.landing-footer-links a:hover { text-decoration: underline; }

.landing-footer-note {
  color: var(--text-muted);
  font-size: 0.78rem;
  font-style: italic;
}

/* --- Wide screens -----------------------------------------------------------
   860px is where `.landing-hero` has room for the pitch and the sign-in card side by side. Below it
   they stack, pitch first, and the "Sign in" pill in the bar jumps to the card. */
@media (min-width: 860px) {
  .landing-hero {
    flex-direction: row;
    align-items: flex-start;
    gap: 48px;
    padding: 44px 0 8px;
  }

  .landing-pitch { flex: 1 1 auto; min-width: 0; }

  /* `.login-card` is `max-width: 400px` already; this stops it shrinking below that in the row. */
  .landing-hero .login-card { flex: 0 0 400px; }

  /* `.brand-header` centres itself, which was right when it sat in a lone card on an empty page. In
     the hero it heads a left-aligned column, so it goes with the column. The card — including the App
     Store lockup, which stays inside it — keeps its own centring. */
  .landing-pitch .brand-header { text-align: left; }

  .landing-headline { font-size: 2.4rem; }
  .landing-lead { font-size: 1.08rem; }

  .landing-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px; }
  .landing-grid.landing-grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }

  .shot-strip {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 24px;
    overflow: visible;
  }

  .shot { flex: none; }

  .landing-section { padding-top: 56px; }
  .landing-section > h2 { font-size: 1.6rem; }
}

/* ===== The Dive Log's Map view: a first-party globe (T-559 / T-561 / T-562; web-only, below the marker) =====
   The List | Map toggle reuses the frozen `.unit-toggle` / `.unit-btn` pill from the calculators
   (above the marker — REUSED, never edited); these two rules undo the pieces of it that were written
   for a full-width row inside a card: its 20 px bottom margin, and `flex: 1` on each button, which
   would stretch two words across the whole header. */
.dives-view-toggle { margin-bottom: 0; flex: 0 0 auto; }
.dives-view-toggle .unit-btn { flex: 0 0 auto; padding: 6px 14px; }

/* The list is hidden by a CLASS on the tab, not by `hidden` on these two: the SSI code owns
   `#dive-ssi-bar`'s `hidden` attribute, and two writers of one attribute disagree. The cards stay in
   the DOM while the globe shows — `.dive-card` count is unchanged, nothing is re-rendered on the way
   back, and `refreshDiveCardCodes` still finds every card. */
#tab-dives.dives-view-map #dive-list-container,
#tab-dives.dives-view-map #dive-ssi-bar { display: none; }

.dive-globe-panel { display: flex; flex-direction: column; gap: 12px; }
.dive-globe-main { flex: 1 1 0; min-width: 0; }
.dive-globe-side { min-width: 0; }

/* A FIXED height, like `.dive-map-box`: the canvas bitmap is sized from `clientHeight`, and a plot
   whose height came from its (empty) content would measure 0 and paint nothing. The three heights
   are in ASCENDING source order — base, then the two media tiers — because every rule here has the
   same specificity and the last one wins (the same-specificity trap the profile chart's tiers hit).
   `touch-action: none` hands pinch and drag to the pointer handlers instead of the page.
   `rgba(255, 255, 255, 0.03)` is the one literal in this block; it is the same literal `.dive-card`
   uses, because the plot is a card-coloured well and not a new colour. */
.dive-globe-plot { position: relative; height: 360px; width: 100%; min-width: 0; border: 1px solid var(--border);
  border-radius: 16px; overflow: hidden; background: rgba(255, 255, 255, 0.03); cursor: grab; touch-action: none; outline: none; }
.dive-globe-plot:focus-visible { box-shadow: 0 0 0 2px var(--primary-glow); }
.dive-globe-plot.dragging { cursor: grabbing; }
.dive-globe-canvas { display: block; }

/* The pins are real <button>s laid over the canvas. The LAYER ignores the pointer so a drag that
   starts between two pins reaches the plot; each PIN takes it back. `transform: translate(-50%, -50%)`
   centres a pin on the projected point, so `left`/`top` are the point itself and the harness can
   compare a pin's rect centre with `globeProject` directly. */
.dive-globe-pins { position: absolute; inset: 0; pointer-events: none; }
.dive-globe-pin { position: absolute; transform: translate(-50%, -50%); pointer-events: auto; padding: 0;
  width: 12px; height: 12px; border-radius: 50%; border: 2px solid var(--bg); background: var(--primary);
  box-shadow: 0 0 6px var(--primary-glow); color: var(--bg); font-size: 0.7rem; font-weight: 700; line-height: 1; cursor: pointer; }
.dive-globe-pin.cluster { background: var(--accent); border-color: var(--primary); }
.dive-globe-pin.selected { box-shadow: 0 0 0 3px var(--primary); }
.dive-globe-pin:focus-visible { outline: 2px solid var(--text); outline-offset: 1px; }

.dive-globe-zoombar { display: flex; gap: 8px; justify-content: center; margin-top: 10px; }
.dive-globe-zoombar .btn-secondary { flex: 0 0 auto; }              /* the copied .btn-secondary is flex: 1 */
.dive-globe-status { color: var(--text-muted); font-size: 0.8rem; margin-top: 8px; text-align: center; }
.dive-globe-note { color: var(--warning); font-size: 0.8rem; margin-top: 6px; text-align: center; }
.dive-globe-note .btn-secondary { flex: 0 0 auto; margin-left: 8px; }
.dive-globe-credit { color: var(--text-muted); font-size: 0.7rem; margin-top: 6px; text-align: right; }

.dive-globe-card { background: var(--card-bg); border: 1px solid var(--border); border-radius: 16px; padding: 14px;
  margin-bottom: 12px; max-height: 420px; overflow: auto; }
.dive-globe-card-site { font-weight: 700; font-size: 1rem; overflow-wrap: anywhere; }
.dive-globe-card-date { color: var(--text-muted); font-size: 0.85rem; margin-bottom: 8px; }
.dive-globe-card dl { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; font-size: 0.9rem; margin: 0; }
.dive-globe-card dt { color: var(--text-muted); }
.dive-globe-card dd { margin: 0; }
.dive-globe-card-actions { display: flex; gap: 8px; margin-top: 10px; }
.dive-globe-card-actions .btn-secondary { flex: 0 0 auto; }
.dive-globe-rows { list-style: none; display: flex; flex-direction: column; gap: 6px; margin: 0; padding: 0; }
.dive-globe-row, .dive-globe-place { width: 100%; text-align: left; overflow-wrap: anywhere; }
.dive-globe-places-title { margin-top: 0; }
.dive-globe-places { list-style: none; display: flex; flex-direction: column; gap: 6px; max-height: 480px; overflow: auto; margin: 0; padding: 0; }
.dive-globe-place.current { border-color: var(--primary); color: var(--text); }

@media (min-width: 900px) {
  .dive-globe-plot { height: 480px; }
  .dive-globe-panel { flex-direction: row; align-items: flex-start; gap: 16px; }
  .dive-globe-side { flex: 0 0 320px; }
}
@media (min-width: 1200px) {
  .dive-globe-plot { height: 560px; }
}

/* ===== Globe round 2 (owner feedback, 2026-09-21) ===== */
/* A pin whose every dive was placed by its SITE NAME rather than a position of its own: hollow, so a
   borrowed position never looks like a measured one. The card says the same in words. */
.dive-globe-pin.approx { background: transparent; border: 2px solid var(--primary); color: var(--text); box-shadow: none; }
.dive-globe-pin.approx.cluster { border-color: var(--accent); }

/* "Not on the globe yet": the dives no pin can carry, one row per site name, with the way to fix it. */
.dive-globe-unplaced { margin-top: 16px; }
.dive-globe-unplaced-note { color: var(--text-muted); font-size: 0.8rem; margin: 0 0 8px; }
.dive-globe-unplaced-list { list-style: none; display: flex; flex-direction: column; gap: 6px; margin: 0; padding: 0; }
.dive-globe-unplaced-row { display: flex; align-items: center; justify-content: space-between; gap: 8px;
  border: 1px solid var(--border); border-radius: 10px; padding: 8px 10px; }
.dive-globe-unplaced-name { overflow-wrap: anywhere; font-size: 0.9rem; }
.dive-globe-unplaced-row .btn-secondary { flex: 0 0 auto; white-space: nowrap; }

/* The dive form's Position note, under the Latitude / Longitude row. */
.dive-position-note { color: var(--text-muted); font-size: 0.78rem; margin: -6px 0 14px; }

/* ===== Add a shared dive (T-609) ===== */
.import-share-form { display: flex; gap: 8px; align-items: stretch; }
.import-share-form input { flex: 1 1 auto; min-width: 0; padding: 10px 12px; border-radius: 10px; border: 1px solid var(--border);
  background: rgba(0, 0, 0, 0.3); color: var(--text); font: inherit; font-size: 0.9rem; }
.import-share-form input:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }
.import-share-form .btn-secondary { flex: 0 0 auto; white-space: nowrap; }
.import-share-preview { border: 1px solid var(--border); border-radius: 12px; padding: 12px 14px; margin-top: 10px; }
.import-share-site { font-weight: 700; font-size: 1rem; overflow-wrap: anywhere; }
.import-share-date { color: var(--text-muted); font-size: 0.85rem; margin-bottom: 8px; }
.import-share-preview dl { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; font-size: 0.9rem; margin: 0; }
.import-share-preview dt { color: var(--text-muted); }
.import-share-preview dd { margin: 0; overflow-wrap: anywhere; white-space: pre-line; }
.import-share-actions { margin-top: 10px; }
.import-share-actions .btn-primary { flex: 0 0 auto; }

/* ===== The notifications bell (T-616) =====
   Owner, 2026-09-23: "some sort of notification in the righthand corner that shows my notifications
   even when im not logged in as the administrator ... for all admins too". Built at runtime by
   `buildNotifications()` in site/index.html for an operator whose permissions include a kind in
   `NOTIFICATION_KINDS`; a diver's DOM has none of these nodes, so none of this applies to them and
   their header is byte-for-byte the row it was. Below the "web-only additions" marker like every
   header rule.

   ## Where it sits, and who pays for it
   Inserted into `.top-actions` immediately BEFORE `.top-user`, so the username menu stays at the
   right edge and the bell is the thing beside it. `flex: 0 0 auto` because the bell must not shrink
   — at 18px there is nothing to give up, and its whole point is to be seen. What it costs the row is
   paid by the username LABEL, the one shrinkable thing in the bar: see the sibling rule at the end of
   this block and the measured table beside it.

   The wrapper does what `.top-user` and `.top-menu` do: `position: relative` is what the
   absolutely-positioned menu anchors to. */
.top-notify {
  position: relative;
  display: flex;
  align-items: center;
  flex: 0 0 auto;
  min-width: 0;
}

/* Takes `.top-user-trigger`'s type treatment — the muted colour, the 10/4/8 padding rhythm, the
   transparent 2px underline sitting on the bar's hairline — so the bell and the name read as one
   family of control. `position: relative` is for the badge. */
.top-notify-trigger {
  position: relative;
  display: flex;
  align-items: center;
  /* ⚠️ `align-self: stretch` and NO vertical padding — MEASURED, the first cut was wrong. With the
     tabs' 10/8 padding around an 18px glyph the bell was a 38px box while every neighbour is 33–36
     (their height comes from a text line, which shrinks with the font clamp at phone widths), so an
     OPERATOR's bar stood 2–5px taller than a DIVER's and the active tab's 2px underline floated
     0–1.5px above the hairline it is built to sit on (the visual verifier's table, 320→1400). A
     stretched item takes the row's height from its neighbours instead of dictating it, so the bell
     is exactly as tall as the tabs at every width and shares their margin-bottom hairline trick. */
  align-self: stretch;
  flex: 0 0 auto;
  min-width: 0;
  background: none;
  border: none;
  color: var(--text-muted);
  cursor: pointer;
  padding: 0 4px;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;   /* On the bar's hairline, like `.top-tab` and `.top-user-trigger`. */
  transition: color 0.15s ease;
}

.top-notify-trigger:hover { color: var(--text); }

.top-notify-trigger:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
  border-radius: 6px;
}

/* An INLINE SVG drawn in `currentColor`: no external request (the rule every header glyph follows —
   the chevrons are CSS triangles for the same reason), no icon font, and it tracks the hover colour
   with no second rule. `display: block` so the inline-box baseline does not add descender space
   under it and skew the underline against its neighbours. */
.top-notify-trigger svg {
  display: block;
  width: 18px;
  height: 18px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* The badge: a small filled pill at the bell's top-right. `--primary` on `--bg` text — two existing
   custom properties, no new colour. `hidden` (the load-bearing `[hidden]` rule at the top of this
   web-only section) when the count is 0; "99+" above 99, so the pill has a widest possible shape.
   `pointer-events: none` so a tap on the number is a tap on the bell.

   ⚠️ `right: 0`, INSIDE the trigger's box, and that was measured rather than chosen: the first cut had
   `right: -3px` for a pill that hangs off the glyph's corner, and an absolutely-positioned child past its
   `position: relative` parent's edge is SCROLLABLE OVERFLOW — the trigger reported `scrollWidth 29 >
   clientWidth 26` at every width, which is the exact signature the header's overflow walk fails on. The
   pill now sits over the bell's top-right quadrant instead of beside it. */
.top-notify-count {
  position: absolute;
  top: 2px;
  right: 0;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  box-sizing: border-box;
  border-radius: 8px;
  background: var(--primary);
  color: var(--bg);
  font-size: 0.65rem;
  font-weight: 700;
  line-height: 16px;
  text-align: center;
  white-space: nowrap;
  pointer-events: none;
}

/* The menu carries `.top-user-menu` as well as this class, so its surface, its `right: 0` anchoring
   and its z-index are the username menu's — one rule, so the two dropdowns cannot end up with
   different geometry. Its rows are `.top-user-item`s: anchors to the console pages, or the one
   "Nothing new" span, which is not interactive and must not light up like a row that is. */
.top-notify-menu a.top-user-item { display: block; text-decoration: none; }

.top-notify-menu .top-notify-empty {
  color: var(--text-muted);
  cursor: default;
}

.top-notify-menu .top-notify-empty:hover { background: none; color: var(--text-muted); }

/* ## What the bell costs the row, and who pays — MEASURED (T-616)
   The bell is 26px (an 18px glyph plus 4+4px of padding) and `.top-actions` adds its 4px gap: 30px
   the row did not carry before. This file records three times over what happens when a header
   addition goes unpaid at 320px — the wordmark is `min-width: 0` + `nowrap`, so the flexbox shrinks
   it and its GLYPHS are cut off inside a box `scrollWidth == clientWidth` calls fine (see the
   `.top-user-trigger` cap and its two overrides in the menu section). So the username label, the one
   shrinkable thing in the bar, pays: WHEN the bell is present — and only then, which is what the
   adjacent-sibling selector says — the trigger's cap comes down by the bell's 30px. A diver's header
   has no `.top-notify`, so their cap is exactly the `min(29vw, 200px)` measured before.

   ⚠️ BOTH arms come down, not only the vw one, and the 768 row is why: `min(calc(29vw - 30px), 200px)`
   was measured first, and at 768 the 200px arm was the one governing — so the cap fell by 7.3px (222.7 →
   192.7) while the bell cost 30, and the wordmark's glyphs measured 108.5px in a 107.2px box, the
   `top-lockup 143>141` overflow this file warns about three times. `170px` is 200 less the bell.

   MEASURED (`tools/notifications-browser-test.mjs`, Range rects per text node, the bell and a badge
   present, the owner's 24-character fixture email as the label):

     | width | wordmark glyphs / box | label box (of trigger cap) | bell   | nav→bell gap | doc     |
     |-------|-----------------------|----------------------------|--------|--------------|---------|
     | 320   | 78.8 / 78.8           | label HIDDEN (see below)   | 22px*  | 4px          | 320/320 |
     (* 22px under 360: the narrow-phone rule below trims the bell to 2px sides like the other triggers.)
     | 375   | 90.7 / 90.7           | 30.8 (78.75)               | 26px   | 4px          | 375/375 |
     | 414   | 98.8 / 98.8           | 42.0 (90.06)               | 26px   | 4px          | 414/414 |
     | 768   | 108.5 / 108.5         | 122.0 (170)                | 26px   | 4px          | 768/768 |
     | 1400  | 108.5 / 108.5         | 182.0 (290) — no ellipsis  | 26px   | 4px          | 1400/1400 |

   Zero clipped glyphs, zero overflow in any ancestor of the bell, the trigger or the wordmark. */
.top-notify + .top-user .top-user-trigger { max-width: min(calc(29vw - 30px), 170px); }

@media (min-width: 900px) {
  .top-notify + .top-user .top-user-trigger { max-width: 290px; }
}

/* ## Under 360px the bell costs the NAME, and the avatar stands in for it — MEASURED, not chosen
   With the cap at 62.8px at 320 the label's box measured 18.8px: the avatar (22), two gaps (10), the
   chevron (8) and the 2px padding this width already uses leave that much, which is one glyph and an
   ellipsis — a label in name only. So below 360, when the bell is present, the trigger drops the label
   and keeps the avatar (the diver's initial, or their photo) and the chevron. The full name stays in
   the trigger's `title` and on the Profile page, and the row's 23.8px goes back to slack. A diver's
   header at 320 is untouched — no `.top-notify`, no rule. ⚠️ The same breakpoint as the header's own
   359px query above, so the two narrow-phone rules move together. */
@media (max-width: 359px) {
  .top-notify + .top-user .top-user-label { display: none; }
  /* The header's own 359px rule narrows `.top-user-trigger` and `.top-menu-trigger` to 2px sides;
     the bell follows it, so the row's budget is spent the same way by every control in it. */
  .top-notify-trigger { padding-left: 2px; padding-right: 2px; }
}

/* --- The Weight Calculator's range (T-625) ----------------------------------
   TWO rules, and that is the whole panel: everything else it needs — the card, the pills, the
   form rows, `.refill-summary-row` for the breakdown, `.refill-note`, `.refill-caveat`,
   `.status-badge.warning` for the refused override and `.share-check` for the tick — already
   exists above, and reusing it is what makes this panel look like the other three rather than
   like a fifth opinion about what a calculator is.

   ## Why the headline is bigger than anything else in the card
   The range IS the answer, and the five breakdown rows under it are how it was reached. At
   `.refill-summary-row`'s 0.8rem the answer would read as one row among six. `tabular-nums`
   because the figures change as the diver types and proportional digits make the whole line
   jump sideways on every keystroke. */
.weight-range {
  font-size: 1.5rem;
  font-weight: 700;
  color: var(--text);
  font-variant-numeric: tabular-nums;
  /* An en dash is not a break opportunity in every engine, and "21–23 lb" must not be able to
     overflow the card on a 320px screen. */
  overflow-wrap: anywhere;
}

/* The same answer in the other unit, deliberately quieter: it is a second reading of one number,
   not a second number. Muted and small enough that the eye takes the headline first, and close
   enough under it to read as belonging to it. */
.weight-range-alternate {
  margin-top: 2px;
  font-size: 0.85rem;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
