/* =======================================================================
 *           #############..........           followtheshadow
 *        ###################.........
 *       #####################.........        app.css
 *      #######################.........       All of the app's styling.
 *     #########################.........
 *     #########################.........
 *     #########################.........
 *      #######################.........       Copyright (C) 2026
 *       #####################.........        1iTurtle LLC
 *        ###################.........         followtheshadow.com
 *           #############..........
 *
 *  SPDX-License-Identifier: AGPL-3.0-or-later   (full text: LICENSE)
 * =======================================================================
 *  Source:   github.com/1iTurtle-LLC/followtheshadow
 *  Contact:  app@followtheshadow.com
 *  EYE SAFETY: never look at the Sun through binoculars, a camera or a
 *  telescope without a certified solar filter. Use ISO 12312-2 eclipse
 *  glasses, except during totality itself.
 *  NO WARRANTY: provided "as is", just for fun, without warranty of any
 *  kind; accuracy of times, paths and weather is not guaranteed. Use at
 *  your own risk.
 * ======================================================================= */

    /* ── Design tokens ───────────────────────────────────────────────── */

    :root {
      --bg:          #0a0c0f;
      --bg2:         #0f1218;
      --bg3:         #141820;
      --border:      #253040;
      --border2:     #334455;
      --gold:        #c8a96e;
      --gold2:       #e8c98e;
      --gold-dim:    #6b5a38;
      --edge:        #9a8a63;   /* interactive edges: hover, focus, active outline */
      --text:        #dde3ec;
      --text-dim:    #7e8fa0;
      --text-pale:   #8e9aaa;
      --red:         #e05c5c;
      --blue:        #5c9ee0;
      --green:       #5ce0a0;
      --pin-red:     #cc2200;
      --mono:        'JetBrains Mono', 'Cascadia Code', Consolas, monospace;
      --serif:       'Cormorant Garamond', Georgia, serif;
      --radius:      4px;
      --tab-h:       3rem;
      --header-h:    3.4rem;   /* slimmed: subtitle removed, so no two-line stack */
    }


    /* ── Reset ───────────────────────────────────────────────────────── */

    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

    body {
      background:  var(--bg);
      color:       var(--text);
      font-family: var(--mono);
      font-size:   1rem;
      line-height: 1.6;
      /* iOS Safari silently inflates the font-size of paragraphs it judges to
         be "the main readable text" of a narrow column — a per-paragraph
         heuristic, not a stylesheet override, which is why a short one-line
         hint can sit right next to a longer paragraph rendered visibly
         bigger even though both share the same declared font-size (this hit
         .log-empty-hint). Disabling it here is global on purpose: any long
         paragraph (the About panel's prose, for one) is equally exposed. */
      -webkit-text-size-adjust: 100%;
              text-size-adjust: 100%;
      /* Pin to the viewport. Plain height:100vh lets iOS Safari scroll the page up
         to tuck its address bar away, dragging the banner + tab bar off-screen
         (out of reach). position:fixed locks the app so that can't happen. */
      position:    fixed;
      top:       0; right: 0; bottom: 0; left: 0;
      overflow:    hidden;
      display:     flex;
      flex-direction: column;
    }


    /* ── Header ──────────────────────────────────────────────────────── */

    .app-header {
      /* #R5 — neither the header nor the tab bar is a .tab-panel, so the
         pan-y rule there never reached them and they still pinched. Nothing
         here scrolls, so refuse touch gestures outright. */
      touch-action:  none;
      flex-shrink:   0;
      display:       flex;
      align-items:   center;
      gap:           1rem;
      padding:       0 1.5rem;
      height:        var(--header-h);
      border-bottom: 1px solid var(--border);
      background:    var(--bg);
    }

    /* Installed PWA: drop the branding banner — the OS launch splash + the
       About panel cover identity; don't waste standalone screen space on it.
       Media query covers modern browsers; .is-standalone (set in index.html)
       is the fallback for older iOS, which lacks display-mode. Zero --header-h
       too so the panel max-height calc (uses --header-h) reclaims the space. */
    @media all and (display-mode: standalone) {
      :root { --header-h: 0rem; }
      .app-header { display: none; }
    }
    .is-standalone { --header-h: 0rem; }      /* .is-standalone sits on <html> = :root */
    .is-standalone .app-header { display: none; }

    /* KEEP THE TAB BUTTONS OUT FROM UNDER THE STATUS BAR. Dropping the header in
       standalone leaves the tab bar as the topmost element, and body is
       position:fixed;inset:0 under viewport-fit=cover — so the bar's top edge is
       the physical top of the screen, with the clock and signal bars sitting on
       top of the buttons.
       Pad rather than shift: the bar GROWS by the inset so its background fills
       the status-bar strip (a shifted bar would leave a transparent gap there),
       while the buttons inside start below it. env() reports 0 anywhere without
       an inset, so nothing else moves.
       Both selectors, as above — the media query for modern browsers, the class
       for older iOS that lacks display-mode. */
    @media all and (display-mode: standalone) {
      .tab-bar {
        height:      calc(var(--tab-h) + env(safe-area-inset-top, 0px));
        padding-top: env(safe-area-inset-top, 0px);
      }
    }
    .is-standalone .tab-bar {
      height:      calc(var(--tab-h) + env(safe-area-inset-top, 0px));
      padding-top: env(safe-area-inset-top, 0px);
    }

    /* The banner mark is the LOCKSCREEN ICON itself (icons/mark.png — the same
       corona art as icons/icon-512-v2.png, background keyed out and cropped
       tight). It is used as a MASK, not an <img>, so it still takes --gold like
       the ☉ glyph it replaced and still glows. Change the mask to a different
       file and the colour follows automatically; set `background: #fff` for the
       white version that appears on the home screen. */
    .app-title {
      font-family:    var(--serif);
      font-size:      1.5rem;
      font-weight:    300;
      letter-spacing: 0.12em;
      color:          var(--gold2);
      text-transform: uppercase;
    }



    /* ── Tab bar ─────────────────────────────────────────────────────── */

    .tab-bar {
      touch-action:  none;   /* #R5 — see .app-header */
      flex-shrink:   0;
      display:       flex;
      height:        var(--tab-h);
      border-bottom: 1px solid var(--border2);
      background:    #070911;
    }

    .tab-btn {
      flex:           1;
      background:     transparent;
      border:         none;
      border-bottom:  2px solid transparent;
      color:          var(--text-dim);
      font-family:    var(--mono);
      font-size:      0.72rem;
      letter-spacing: 0.12em;
      text-transform: uppercase;
      cursor:         pointer;
      transition:     all 0.15s;
      padding:        0 0.5rem;
    }
    /* Hamburger as an inline SVG, not the ☰ glyph: the glyph renders hairline-thin on
       iOS and fuses into a block when stroked. Solid rounded bars read cleanly at any
       size and match the visual weight of the colour-emoji tab icons. */
    .tab-icon-svg {
      width: 1.15em;
      height: 1.15em;
      vertical-align: -0.2em;
      /* currentColor: every tab icon is SVG now, so the icon simply takes the
         button's colour and the active state is declared once, on the button. */
      fill: currentColor;
      filter: drop-shadow(0 1px 1px rgba(0,0,0,0.35));
    }

    .tab-btn:hover    { color: var(--text); }
    .tab-btn.active   {
      color:        var(--gold);
      border-bottom-color: var(--gold);
      background:   var(--bg);
    }


    /* ── Tab content area ────────────────────────────────────────────── */

    .tab-content {
      flex:     1;
      overflow: hidden;
      position: relative;
    }

    .tab-panel {
      position:  absolute;
      top:     0; right: 0; bottom: 0; left: 0;
      overflow-y: auto;
      /* #R5 — iOS Safari ignores user-scalable=no on purpose (accessibility),
         so page pinch-zoom has to be refused per element. pan-y keeps vertical
         scrolling and drops the pinch. #tab-map RE-WIDENS this below: it is a
         .tab-panel too, and touch-action intersects down the ancestor chain,
         so leaving it at pan-y here would kill horizontal map panning. */
      touch-action: pan-y;
      padding:   1.25rem 1.5rem 2rem;
      /* .detail-title reads this to bleed its sticky background edge-to-edge
         (see below) — kept in sync with the padding above so it never has to
         be duplicated by hand at each breakpoint/context. */
      --panel-pad-x: 1.5rem;
      display:   none;
    }
    .tab-panel.active { display: block; }

    /* Constrain content width on wide screens */
    .tab-panel > * {
      max-width: 860px;
      margin-left: auto;
      margin-right: auto;
    }

    /* Map panel has no padding — map fills edge to edge */
    #tab-map {
      padding:  0;
      overflow: hidden;  /* map fills the panel; no outer scroll */
      /* Undo the .tab-panel pan-y (#R5). An ancestor's touch-action narrows a
         descendant's, so pan-y here would stop the map panning sideways. */
      touch-action: pan-x pan-y;
    }
    #tab-map > * {
      max-width: none;
    }

    /* Shown by js/map.js only when the browser has no WebGL2 (paths can't draw). */
    #webgl-notice {
      position:   absolute; top: 4rem; left: 50%;
      -webkit-transform: translateX(-50%); transform: translateX(-50%);
      max-width:  calc(100% - 2rem);
      z-index:    20;
      padding:    0.6rem 0.9rem;
      background: var(--bg2);
      border:     1px solid var(--gold);
      border-radius: 6px;
      color:      var(--text);
      font-size:  0.85rem;
      text-align: center;
      cursor:     pointer;
    }
    #map {
      touch-action: pan-x pan-y;  /* allow map panning but not page scroll */
    }


    /* ── Common panel/section styles ─────────────────────────────────── */

    .panel {
      background:    var(--bg2);
      border:        1px solid var(--border);
      border-radius: var(--radius);
      padding:       1.25rem;
      margin-bottom: 1.25rem;
    }

    .section-label {
      font-family:    var(--mono);
      font-size:      0.68rem;
      letter-spacing: 0.2em;
      text-transform: uppercase;
      color:          var(--text-dim);
      margin-bottom:  0.65rem;
    }

    /* Top-level section headers — used for Global Circumstances and
       Local Circumstances. Stronger than .section-label / .subsection-label. */
    .top-section-header {
      font-family:    var(--mono);
      font-size:      0.92rem;
      font-weight:    700;
      letter-spacing: 0.18em;
      text-transform: uppercase;
      color:          var(--gold);
      margin-bottom:  0.75rem;
      padding-bottom: 0.5rem;
      border-bottom:  1px solid var(--border);
    }

    /* Inline coords appended to a top-section-header — normal case, smaller,
       so the location reads at a glance without claiming its own row.
       On narrow screens it wraps to its own line below the heading. */
    .header-coords {
      margin-left:    0.75rem;
      font-size:      0.7rem;
      letter-spacing: 0.04em;
      text-transform: none;
      color:          var(--text-dim);
    }

    @media (max-width: 480px) {
      .header-coords {
        display:     block;
        margin-left: 0;
        margin-top:  0.2rem;
      }
    }


    /* ── Form controls ───────────────────────────────────────────────── */

    .field { margin-bottom: 1rem; }
    .field:last-child { margin-bottom: 0; }

    label {
      display:        block;
      font-family:    var(--mono);
      font-size:      0.68rem;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      color:          var(--text-dim);
      margin-bottom:  0.35rem;
    }

    /* Form controls — applies to select + textarea. The only remaining inputs
       are type=range sliders, which have their own block below and override
       most of these properties anyway. */
    select, textarea {
      width:         100%;
      background:    var(--bg3);
      border:        1px solid var(--border2);
      border-radius: var(--radius);
      color:         var(--text);
      font-family:   var(--mono);
      font-size:     0.88rem;
      padding:       0.5rem 0.7rem;
      outline:       none;
      transition:    border-color 0.2s;
      appearance:    none;
    }
    select:focus, textarea:focus { border-color: var(--edge); }

    select {
      background-image:    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%236b7788'/%3E%3C/svg%3E");
      background-repeat:   no-repeat;
      background-position: right 0.75rem center;
      padding-right:       2rem;
      cursor:              pointer;
    }


    /* ── Buttons ─────────────────────────────────────────────────────── */

    .btn {
      font-family:    var(--mono);
      font-size:      0.75rem;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      padding:        0.55rem 1.2rem;
      border-radius:  var(--radius);
      border:         none;
      cursor:         pointer;
      transition:     all 0.2s;
      white-space:    nowrap;
    }
    .btn-primary {
      background:  var(--gold);
      color:       var(--bg);
      font-weight: 700;
    }
    .btn-primary:hover    { background: var(--gold2); box-shadow: 0 0 20px rgba(200,169,110,0.25); }
    .btn-primary:disabled { background: var(--gold-dim); color: var(--bg2); cursor: not-allowed; box-shadow: none; }

    .btn-ghost {
      background: transparent;
      color:      var(--text-dim);
      border:     1px solid var(--border2);
    }
    .btn-ghost:hover { border-color: var(--edge); color: var(--text); }

    .btn-danger {
      background: transparent;
      color:      var(--red);
      border:     1px solid rgba(224,92,92,0.3);
    }
    .btn-danger:hover { background: rgba(224,92,92,0.1); border-color: var(--red); }


    /* ── Search tab ──────────────────────────────────────────────────── */

    .search-box {
      position:      relative;
      margin-bottom: 0.75rem;
    }
    .search-box textarea {
      box-sizing:    border-box;
      field-sizing:  content;          /* native auto-height on supporting browsers */
      padding-right: 2.5rem;
      font-size:     0.75rem;
      resize:        none;
      overflow:      hidden;
    }
    .search-clear {
      position:   absolute;
      right:      0.5rem;
      top:        0.5rem;
      background: none;
      border:     none;
      color:      var(--text-dim);
      cursor:     pointer;
      font-size:  1rem;
      padding:    0.25rem;
      line-height: 1;
    }
    .search-clear:hover { color: var(--text); }

    .search-hint {
      font-family: var(--mono);
      font-size:   0.65rem;
      color:       var(--text-dim);
      margin-bottom: 0.75rem;
      line-height: 1.6;
    }
    .search-hint strong { color: var(--text-pale); font-weight: 400; }

    .pill-row {
      display:       flex;
      gap:           0.4rem;
      flex-wrap:     wrap;
      margin-bottom: 0.75rem;
    }

    .pill {
      font-family:    var(--mono);
      font-size:      0.68rem;
      letter-spacing: 0.06em;
      padding:        0.35rem 0.75rem;  /* taller for touch */
      border-radius:  20px;
      border:         1px solid var(--border2);
      background:     transparent;
      color:          var(--text-dim);
      cursor:         pointer;
      transition:     all 0.15s;
      min-height:     2rem;             /* ~32px minimum touch target */
    }
    .pill:hover  { border-color: var(--edge); color: var(--text); }
    .pill.active { background: rgba(200,169,110,0.12); border-color: var(--edge); color: var(--gold); }
    .pill.pill-loc { background: rgba(200,169,110,0.1); border-color: var(--edge); color: var(--gold); }

    /* Obscuration slider */
    .osc-row {
      display:     flex;
      align-items: center;
      gap:         0.75rem;
      margin-bottom: 0.75rem;
    }
    .osc-label {
      font-family: var(--mono);
      font-size:   0.68rem;
      color:       var(--text-dim);
      white-space: nowrap;
      min-width:   7rem;
    }
    .osc-value {
      font-family: var(--mono);
      font-size:   0.72rem;
      color:       var(--gold2);
      min-width:   3rem;
      text-align:  right;
    }
    input[type=range] {
      flex:       1;
      padding:    0;
      background: transparent;
      border:     none;
      accent-color: var(--gold);
    }

    /* Scan progress */
    .scan-bar {
      display:     flex;
      gap:         0.6rem;
      align-items: center;
      margin-bottom: 0.75rem;
      flex-wrap:   wrap;
    }
    .scan-progress {
      font-family: var(--mono);
      font-size:   0.7rem;
      color:       var(--text-dim);
      flex:        1;
      min-width:   8rem;
    }
    .progress-track {
      height:        3px;
      background:    var(--border2);
      border-radius: 2px;
      margin-top:    0.3rem;
      overflow:      hidden;
    }
    .progress-fill {
      height:        100%;
      background:    var(--gold-dim);
      border-radius: 2px;
      transition:    width 0.15s;
      width:         0%;
    }

    /* Eclipse list */
    .eclipse-list {
      border:        1px solid var(--border);
      border-radius: 0 0 var(--radius) var(--radius);
      background:    var(--bg3);
      /* Height fills remaining space in tab panel */
      max-height:    calc(100vh - var(--header-h) - var(--tab-h) - 16rem);
      min-height:    8rem;
      overflow-y:    auto;
    }
    .eclipse-list::-webkit-scrollbar       { width: 5px; }
    .eclipse-list::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 3px; }

    .eclipse-item {
      display:               grid;
      grid-template-columns: 2rem 1fr 4.5rem;
      align-items:           center;
      gap:                   0.5rem;
      padding:               0.65rem 0.75rem;
      cursor:                pointer;
      border-bottom:         1px solid var(--border);
      font-family:           var(--mono);
      font-size:             0.88rem;
      transition:            background 0.1s;
    }
    .eclipse-item:last-child { border-bottom: none; }
    .eclipse-item:hover      { background: rgba(255,255,255,0.03); }
    .eclipse-item.selected   { background: rgba(200,169,110,0.15); border-left: 3px solid var(--gold); color: var(--gold); }

    .et-T { color: var(--gold2); }
    .et-A { color: var(--blue); }
    .et-H { color: var(--green); }
    .et-P { color: var(--text-dim); }

    .list-status {
      padding:     1rem;
      font-family: var(--mono);
      font-size:   0.76rem;
      color:       var(--text-dim);
      text-align:  center;
    }

    /* Selected eclipse bar */
    /* ── Data tab ────────────────────────────────────────────────────── */

    .no-selection {
      text-align:  center;
      padding:     3rem 1rem;
      color:       var(--text-dim);
      font-style:  italic;
      font-size:   1.05rem;
    }

    .result-header {
      margin-bottom:   1.25rem;
      padding-bottom:  1rem;
      border-bottom:   1px solid var(--border);
    }
    .result-title {
      font-size:   1.5rem;
      font-weight: 300;
      font-style:  italic;
      color:       var(--text);
      margin-bottom: 0.2rem;
    }
    .result-title .type-badge {
      font-size:     0.72rem;
      vertical-align: middle;
      position:      relative;
      top:           -0.15rem;
    }
    .type-badge {
      font-family:    var(--mono);
      font-size:      0.7rem;
      letter-spacing: 0.12em;
      text-transform: uppercase;
      padding:        0.25rem 0.75rem;
      border-radius:  20px;
      white-space:    nowrap;
    }
    .badge-total   { background: rgba(200,169,110,0.15); color: var(--gold2);     border: 1px solid var(--gold-dim); }
    .badge-annular { background: rgba(92,158,224,0.15);  color: var(--blue);      border: 1px solid #3a6e9e; }
    .badge-partial { background: rgba(110,119,136,0.15); color: var(--text-pale); border: 1px solid var(--border2); }
    .badge-hybrid  { background: rgba(92,224,160,0.15);  color: var(--green);     border: 1px solid #3a9e70; }

    /* ── Details panel ──────────────────────────────────────────────────
       One type scale, one spacing scale, two columns aligned by a real
       table. Section header, data table, subsection header, contacts
       table — same rhythm throughout. */

    /* Both tiers are GOLD. The hierarchy is carried by SIZE alone: the top tier
       is a clear notch larger and bold, the sub tier stays at its original
       small size. Colour is not doing hierarchy work here, so it is free to
       mark these as headings against the grey of labels and body text. */
    .detail-section-h {
      font-family:    var(--mono);
      font-size:      0.92rem;
      font-weight:    700;
      letter-spacing: 0.15em;
      text-transform: uppercase;
      color:          var(--gold);
      margin:         1.6rem 0 0.5rem;
      padding:        0;
    }
    .detail-section-h:first-child { margin-top: 0; }

    /* SUB level (Contact Times, Sun Track): gold too, at the original small
       size. Against .circ-row .l — same size bracket, but grey and not
       bold — the gold is what says "heading". */
    .detail-sub-h {
      font-family:    var(--mono);
      font-size:      0.72rem;
      font-weight:    700;
      letter-spacing: 0.15em;
      text-transform: uppercase;
      color:          var(--gold);
      margin:         1.5rem 0 0.4rem;
      padding:        0;
      border:         none;
    }

    .detail-section-h .header-coords {
      margin-left:    0.6rem;
      font-size:      0.66rem;
      letter-spacing: 0.04em;
      text-transform: none;
      color:          var(--text-dim);
    }

    /* Container-query root for .circs-grid below: its inline size is the
       sidebar's width on desktop and the full screen on mobile, either way
       the thing .circs-grid actually needs to react to. */
    #data-panel {
      container-name: circs-panel;
      container-type: inline-size;
    }

    /* Data display: a grid of label/value pairs (Local + Global Circumstances).
       Plain flex rows, not a table — that's what lets the grid below reflow
       from one pair per row to two once the container (the sidebar, which the
       user can drag wider — see .sidebar-resize-handle) has the room. Default
       is a single column, identical in effect to the old two-column table. */
    .circs-grid {
      display:               grid;
      grid-template-columns: 1fr;
      column-gap:            1.2rem;
      font-family:           var(--mono);
    }
    /* A value that carries an explanation. `title` was tried first and is not
       fit for this: the browser delays it about a second, gives no hint that
       anything is there, and never appears at all on touch. This shows
       instantly, marks itself with a dotted underline, and is focusable so a tap
       or the keyboard opens it too.

       The row needs position:relative so the bubble anchors to it; that is set
       on .circ-row's own block below, not in a second block here — test_hygiene
       rejects a selector defined twice outside a media query, and rightly: two
       blocks for one selector is how a rule ends up silently overridden.

       Qualified on [data-tip] because these rows start as an ellipsis and fill
       asynchronously, and a failed fill leaves a dash. Without it the underline
       would promise an explanation that never arrives, and hovering would open
       an empty bubble. */
    .circ-tip[data-tip] {
      border-bottom: 1px dotted var(--text-dim);
      cursor:        help;
      outline:       none;
    }
    .circ-tip[data-tip]::after {
      content:       attr(data-tip);
      position:      absolute;
      right:         0;
      bottom:        calc(100% + 0.35rem);
      z-index:       40;
      width:         min(17rem, 78vw);
      white-space:   pre-line;      /* the \n in the text become line breaks */
      text-align:    left;
      font-family:   var(--mono);
      font-size:     0.68rem;
      line-height:   1.5;
      color:         var(--text);
      /* --bg3 and --border, the same pair every other raised surface uses.
         An earlier draft invented var(--panel, #10161d): --panel does not exist,
         so it would silently have used a hardcoded colour and ignored the theme
         — the kind of thing that looks fine until the palette moves. */
      background:    var(--bg3);
      border:        1px solid var(--border2);
      border-radius: 4px;
      padding:       0.5rem 0.6rem;
      box-shadow:    0 6px 18px rgba(0, 0, 0, 0.45);
      opacity:       0;
      visibility:    hidden;
      transition:    opacity 0.12s ease;
      pointer-events: none;
    }
    .circ-tip[data-tip]:hover::after,
    .circ-tip[data-tip]:focus::after,
    .circ-tip[data-tip]:focus-visible::after {
      opacity:    1;
      visibility: visible;
    }

    .circ-row {
      position:        relative;   /* anchors the .circ-tip bubble */
      display:         flex;
      justify-content: space-between;
      align-items:     baseline;
      gap:             0.6rem;
      /* Softer than --border and roomier than before. Nine rows of Global
         Circumstances at full border weight read as a ladder; the eye wants the
         rows grouped, not fenced. Enough contrast to guide a horizontal read,
         not enough to draw attention. */
      padding:        0.3rem 0;
      border-bottom:  1px solid var(--border);
    }
    /* Only the true last row loses its rule — with two columns active the row
       above it may keep one. A minor cosmetic gap, not worth chasing further. */
    .circs-grid > .circ-row:last-child { border-bottom: none; }
    .circ-row .l {
      font-size:      0.66rem;
      letter-spacing: 0.08em;
      text-transform: uppercase;
      color:          var(--text-dim);
      white-space:    nowrap;
      flex:           0 0 auto;
    }
    .circ-row .v {
      font-size:     0.82rem;
      color:         var(--gold2);
      text-align:    right;
      overflow:      hidden;
      text-overflow: ellipsis;
      white-space:   nowrap;
      flex:          1 1 auto;
    }
    /* Two pairs per row once the grid's own container is wide enough to hold
       them without crowding — independent of viewport, so it reacts to the
       sidebar's dragged width, not just desktop-vs-mobile. Needs an ancestor
       with container-type: inline-size; see #data-panel below. */
    @container circs-panel (min-width: 460px) {
      .circs-grid { grid-template-columns: 1fr 1fr; }
    }

    /* Sits directly under "Local Circumstances" and names the place the figures
       below are for. No negative margin any more: the heading's own
       margin-bottom is the gap, so this can't drift when that changes. */
    .detail-subloc {
      font-family:    var(--mono);
      font-size:      0.7rem;
      color:          var(--text-dim);
      margin:         0 0 0.8rem;
      padding:        0;
    }

    /* Contact-times table — single time column (UT), header band, row stripes. */
    .contacts-table {
      width:           100%;
      border-collapse: collapse;
      font-family:     var(--mono);
      font-size:       0.8rem;
      table-layout:    auto;
    }
    .contacts-table th,
    .contacts-table td {
      padding:        0.16rem 0.7rem 0.16rem 0;
      vertical-align: middle;
      white-space:    nowrap;
    }
    .contacts-table th:last-child,
    .contacts-table td:last-child { padding-right: 0.4rem; }
    .contacts-table th {
      text-align:     left;
      font-size:      0.66rem;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      color:          var(--text-dim);
      font-weight:    400;
      padding-bottom: 0.6rem;
      border-bottom:  1px solid var(--border);
    }
    .contacts-table .day-off {
      font-size:  0.65em;
      color:      var(--text-dim);
      margin-left: 0.15rem;
      vertical-align: super;
      line-height: 1;
    }
    .contacts-table th:first-child { padding-left: 0.55rem; }
    .contacts-table th.time-mode-toggle {
      cursor:           pointer;
      color:            var(--text);
      text-decoration:  underline dotted;
      text-underline-offset: 3px;
    }
    .contacts-table th.time-mode-toggle:hover { color: var(--text); }
    .contacts-table th,
    .contacts-table td             { padding-left: 0.55rem; }
    .contacts-table td             { color: var(--text); }
    .contacts-table tbody tr:nth-child(odd) td { background: rgba(255,255,255,0.025); }
    .contacts-table td svg         { vertical-align: middle; margin-right: 0.4rem; }
    .contacts-table .row-umbral td { color: var(--gold2); }
    .contacts-table .row-max    td { color: var(--gold2); font-weight: 700; }

    /* Footnote under the contacts table: small, dim, mono. */
    .contacts-note {
      font-family: var(--mono);
      font-size:   0.66rem;
      color:       var(--text-dim);
      margin:      0.4rem 0 0;
      padding:     0;
    }

    /* Empty / loading states */
    .no-location, .no-eclipse {
      text-align: center;
      padding:    1.5rem 0.5rem;
      color:      var(--text-dim);
      font-family: var(--mono);
      font-size:  0.82rem;
    }

    .note {
      margin-top:  0.6rem;
      font-family: var(--mono);
      font-size:   0.66rem;
      color:       var(--text-dim);
      padding:     0.45rem 0.7rem;
      border-left: 2px solid var(--border);
      line-height: 1.6;
    }

    /* Status message */
    #status-msg {
      font-family: var(--mono);
      font-size:   0.75rem;
      min-height:  1.2rem;
      margin-top:  0.5rem;
      color:       var(--text-dim);
    }
    #status-msg.err { color: var(--red); }


    /* ── Settings tab ────────────────────────────────────────────────── */

    .settings-section { margin-bottom: 2rem; }

    /* Collapsible settings groups */
    .settings-group {
      border:        1px solid var(--border);
      border-radius: var(--radius);
      margin-bottom: 1rem;
      overflow:      hidden;
    }
    .settings-group-header {
      display:        flex;
      align-items:    center;
      font-family:    var(--mono);
      font-size:      0.8rem;
      letter-spacing: 0.14em;
      text-transform: uppercase;
      color:          var(--text);
      padding:        0.75rem 1rem;
      background:     var(--bg2);
      cursor:         pointer;
      user-select:    none;
      list-style:     none;
    }
    .settings-group-header::-webkit-details-marker { display: none; }
    .settings-group-header::after {
      content:    '▾';
      margin-left: auto;
      font-size:  0.7rem;
      color:      var(--text-dim);
      transition: transform 0.2s;
    }
    .settings-group:not([open]) .settings-group-header::after {
      transform: rotate(-90deg);
    }
    .settings-group > :not(summary) {
      padding: 0.75rem 1rem 0;
    }


    .about-text {
      font-family: var(--mono);
      font-size:   0.85rem;
      line-height: 1.6;
      color:       var(--text);
    }
    /* Body text here is --text-dim (#7e8fa0) — every .about-text sits inside a
       .settings-group, and that rule dims it (see below). Emphasis is a SMALL
       step up from that: #96a4b3, a quarter of the way toward --text, 1.3x
       luminance. Still plainly grey.
       Weight does the real work: 700 was invisible because JetBrains Mono's
       bold is a restrained coding weight, so this is 800 (ExtraBold).
       To tune, move along the --text-dim -> --text ramp: #8c9cab is subtler,
       #9facbb is stronger. --text itself (#dde3ec) reads as white here.
       NOT gold — gold is overloaded in this palette. */
    .about-text strong {
      font-weight: 800;
      color:       #96a4b3;
    }

    /* THREE LEVELS in the Instructions, because there were effectively none and
       one button's explanation ran straight into the next.
         .about-sec   Search / Map overlays / Details / User Log   - white
         .about-sub   one button or feature within a section       - bold grey
         <strong>     emphasis inside a sentence                   - bold grey
       .about-sub and <strong> share a colour on purpose: a sub-head is an
       emphasised lead-in, not a different kind of thing. The separation that
       does the work is the WHITE at the top level and the space above each. */
    .about-text .about-sec {
      color:          var(--text);
      font-weight:    800;
      font-size:      0.82rem;
      letter-spacing: 0.04em;
      margin-top:     1.6rem;
      margin-bottom:  0.5rem;
    }
    .about-text .about-sec:first-child { margin-top: 0; }

    .about-text .about-sub {
      font-weight:   800;
      color:         #96a4b3;
      margin-top:    1.1rem;
      margin-bottom: 0.35rem;
    }

    /* A THIRD level, for something that belongs to the feature above it rather
       than standing beside it: Now is a mode of the cloud overlay, and at the
       same indent the two read as equals. Indent carries the relationship; the
       paragraphs under it are indented to match or the heading floats free. */
    .about-text .about-sub2 {
      font-weight:   700;
      color:         #96a4b3;
      margin:        0.9rem 0 0.3rem 1.1rem;
    }
    .about-text .about-indent { margin-left: 1.1rem; }
    .settings-group .about-text { font-size: 0.72rem; color: var(--text-dim); }

    /* Icons quoted inside running prose (see the log block in Instructions).
       These use TIGHT per-icon viewBoxes, unlike the app's buttons which keep
       0 0 20 20 — there the box is the tap target, here it is dead space, and
       each glyph fills a different fraction of it (the arrow only 38% against
       the flag's 69%), so a shared box made them look mismatched.
       Height is set in em so they track the paragraph; vertical-align centres
       the box on the x-height rather than the baseline, which is what makes an
       inline icon sit level with lowercase text instead of riding high. */
    .inline-icon {
      display:        inline-block;
      width:          1.5em;
      height:         1.5em;
      vertical-align: -0.5em;
      color:          var(--text);
    }

    .about-text p  { margin-bottom: 0.75rem; }

    /* Formula block in the Instructions. Monospace so the exponents line up and
       the variable list reads as a key rather than prose. Scrolls sideways on
       its own so a long line never widens the sidebar. */
    .about-formula {
      font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
      font-size: 0.72rem;
      line-height: 1.55;
      color: var(--text);
      background: rgba(255, 255, 255, 0.035);
      border-left: 2px solid var(--edge);
      border-radius: 3px;
      padding: 0.55rem 0.7rem;
      margin: 0 0 0.75rem;
      overflow-x: auto;
      white-space: pre;
    }
    .about-formula .k { color: var(--text-dim); }
    .about-text a  { color: var(--text); text-decoration: underline;
                     text-underline-offset: 0.18em; }
    .about-text a:hover { color: var(--text-pale); }

    .syntax-dl {
      margin:      0.5rem 0 0;
      padding:     0;
    }
    .syntax-entry {
      display:     grid;
      grid-template-columns: 5.5rem 1fr;
      gap:         0 0.75rem;
      padding:     0.35rem 0;
      align-items: baseline;
    }
    .syntax-entry dt {
      font-family:    var(--mono);
      font-size:      0.68rem;
      letter-spacing: 0.08em;
      text-transform: uppercase;
      color:          var(--text);
      font-weight:    700;
      padding-top:    0.1rem;
      align-self:     start;
    }
    .syntax-entry dd {
      margin:      0;
      font-family: var(--mono);
      font-size:   0.73rem;
      color:       var(--text);
      line-height: 1.65;
    }

    /* A qualifying aside inside a syntax dd — smaller, dimmed, and in the body
       face rather than the mono, so it reads as a note about the tokens above
       and not as another token you could type. */
    .syntax-entry dd .syntax-note {
      display:     inline-block;
      margin-top:  0.15rem;
      font-family: inherit;
      font-size:   0.68rem;
      color:       var(--text-dim, var(--text));
      opacity:     0.75;
      line-height: 1.5;
    }


    /* Tolerances table (reuses syntax-table class) */
    .syntax-table {
      width:           100%;
      border-collapse: collapse;
      font-family:     var(--mono);
      font-size:       0.72rem;
      margin:          0.75rem 0;
    }
    .syntax-table th {
      text-align:     left;
      color:          var(--text-dim);
      font-size:      0.65rem;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      padding:        0.4rem 0.5rem;
      border-bottom:  1px solid var(--border2);
      font-weight:    400;
    }
    .syntax-table td {
      padding:        0.4rem 0.5rem;
      border-bottom:  1px solid var(--border);
      vertical-align: top;
    }
    .syntax-table tr:last-child td { border-bottom: none; }
    .syntax-table td:first-child   { color: var(--gold2); white-space: nowrap; }
    .syntax-table td:last-child    { color: var(--text-pale); }



    .detail-title {
      display:       flex;
      align-items:   center;
      gap:           0.6rem;
      font-family:   var(--mono);
      font-size:     1.1rem;
      color:         var(--text);
      padding:       0.25rem 0 0.5rem;
      border-bottom: 1px solid var(--border);
      margin-bottom: 0.5rem;
      position:      sticky;
      top:           -1.25rem;        /* cancel the tab-panel top padding */
      background:    var(--bg);
      z-index:       5;
      margin-top:    -1.25rem;         /* pull up into the padding so it pins flush */
      padding-top:   1.25rem;          /* restore visual top spacing */
      /* Same trick, sideways: without this the sticky bar's opaque background
         only covered the content column, so content scrolling underneath it
         stayed visible in the left/right padding strips — worst on mobile,
         where that strip is the difference between the bar and the actual
         edge of the phone. --panel-pad-x (set on .tab-panel, three different
         values across desktop/sidebar/mobile) keeps this correct everywhere
         .detail-title is used without hardcoding any of them here. */
      margin-left:   calc(-1 * var(--panel-pad-x, 1.5rem));
      margin-right:  calc(-1 * var(--panel-pad-x, 1.5rem));
      padding-left:  var(--panel-pad-x, 1.5rem);
      padding-right: var(--panel-pad-x, 1.5rem);
    }
    /* In the sidebar the panel background is lighter than --bg; match it so the
       pinned header blends with the sheet it sits on. */
    .sidebar .detail-title { background: #1a2030; }

    /* The date takes the slack and ellipsises if it must; the actions never
       shrink. Without min-width:0 a flex item refuses to go below its content
       width, so a long date pushed the buttons onto a second line instead. */
    .detail-title-date {
      flex:          1 1 auto;
      min-width:     0;
      overflow:      hidden;
      text-overflow: ellipsis;
      white-space:   nowrap;
    }
    .detail-title-icon {
      display:     inline-flex;
      align-items: center;
      flex:        0 0 auto;
    }

    /* Title actions: icon-only, square, grouped. They used to carry the words
       "Share" and "Save"; two labelled buttons plus a long date (e.g.
       "12 August 2026") ran past the sidebar width and wrapped the date onto a
       second line. The glyphs plus title/aria-label carry the same meaning in a
       fifth of the space. */
    .detail-actions {
      margin-left: auto;
      display:     flex;
      gap:         0.3rem;
      flex:        0 0 auto;
    }
    .icon-btn {
      display:         flex;
      align-items:     center;
      justify-content: center;
      width:           1.75rem;
      height:          1.75rem;
      padding:         0;
      font-size:       0.9rem;
      line-height:     1;
      cursor:          pointer;
      background:      transparent;
      border:          1px solid var(--border);
      border-radius:   var(--radius);
      color:           var(--text-dim);
      font-family:     var(--mono);
    }
    .icon-btn:hover { border-color: var(--edge); color: var(--text); }
    .icon-btn svg { display: block; width: 0.95rem; height: 0.95rem; }
    /* The star is SVG, not a text glyph: a glyph centres on its LINE BOX
       (which includes ascender and descender space), so it sat visibly high in
       a flex-centred square no matter what line-height was set. An SVG centres
       on its own box. */
    .log-save-btn svg { width: 1.1rem; height: 1.1rem; }

    /* ── User log panel ─────────────────────────────────── */

    /* The saved state of the star button in the detail title (.icon-btn, above,
       with the rest of the title). */
    .log-save-btn.saved { color: var(--gold); border-color: var(--edge); }

    .log-summary {
      padding:        0.6rem 0.75rem;
      font-size:      0.72rem;
      letter-spacing: 0.08em;
      text-transform: uppercase;
      color:          var(--text-dim);
      border-bottom:  1px solid var(--border);
    }

    .log-empty { padding: 1.5rem 1rem; color: var(--text-dim); font-size: 0.85rem; }
    .log-empty-hint { margin-top: 0.6rem; color: var(--text-dim); line-height: 1.7; }
    /* The star glyph (\u2606) has no font stack of its own, so unstyled it falls
       back to the platform's symbol/emoji font — visibly bigger than the body
       text around it, which is what actually looked like "two fonts" on
       mobile. Pin it to the surrounding text's own size and colour instead of
       leaving it to whatever the fallback font's metrics happen to be.
       NOT gold: gold is the star's SAVED state, and this sentence is telling
       you to press the UNSAVED one. Weight alone lifts it off the prose. */
    .log-empty-hint strong {
      font-family: inherit;
      font-size:   1em;
      font-weight: 700;   /* 600 has no face and rounded here anyway (§11.5) */
      color:       inherit;
    }

    .log-item { border-bottom: 1px solid var(--border); }
    .log-item:last-child { border-bottom: none; }
    .log-item.selected .log-date { color: var(--gold); }

    /* Two grid rows. The actions sit on the DATE line, so the coordinates get
       the full width of the row underneath instead of being squeezed into
       whatever the icons leave over — which is what was truncating them.
       The type icon spans both rows and stays at the top.
       The LEFT of the type icon is reserved for the coming t-shirt selection
       checkboxes — do not put anything there. */
    .log-row {
      display:               grid;
      grid-template-columns: auto auto 1fr auto;
      grid-template-areas:   "pick ico date acts"
                             "pick ico loc  loc";
      align-items:           center;
      column-gap:            0.5rem;
      row-gap:               0.1rem;
      padding:               0.55rem 0.6rem;
    }
    .log-row:hover { background: rgba(255,255,255,0.03); }

    .log-ico  { grid-area: ico; display: flex; align-self: start; margin-top: 0.15rem; }
    .log-date { grid-area: date; font-size: 0.88rem; color: var(--text); }

    /* Tight cluster — these three belong together, so they read as one control
       group rather than three scattered buttons. */
    .log-acts {
      grid-area: acts;
      display:   flex;
      gap:       0.05rem;
    }

    .log-loc {
      grid-area:   loc;
      display:     flex;
      align-items: center;
      gap:         0.2rem;
      min-width:   0;
    }
    .log-coords {
      font-size:     0.7rem;
      color:         var(--text-dim);
      overflow:      hidden;
      text-overflow: ellipsis;
      white-space:   nowrap;
    }
    .log-coords.none { font-style: italic; opacity: 0.75; }
    /* What the eclipse does at that spot — a DATA value, so gold2. */
    .log-circ {
      font-size:   0.7rem;
      color:       var(--gold2);
      white-space: nowrap;
      flex:        0 0 auto;
      margin-left: auto;      /* right-justified against the row edge */
    }

    /* Every row action shares one footprint so the icon columns line up. */
    .log-act {
      flex:            0 0 auto;
      display:         flex;
      align-items:     center;
      justify-content: center;
      width:           1.6rem;
      height:          1.6rem;
      padding:         0;
      background:      transparent;
      border:          1px solid transparent;
      border-radius:   var(--radius);
      cursor:          pointer;
      color:           var(--text-dim);
    }
    .log-act svg          { width: 1rem; height: 1rem; display: block; }
    .log-act:hover        { border-color: var(--edge); color: var(--text); }
    .log-act:disabled     { opacity: 0.3; cursor: default; }
    .log-act:disabled:hover { border-color: transparent; color: var(--text-dim); }

    /* The pencil is inline with the coordinates it edits, so it's smaller and
       unbordered — it belongs to that line, not to the row's action cluster. */
    .log-edit-btn        { width: 1.35rem; height: 1.35rem; }
    .log-edit-btn svg    { width: 0.8rem; height: 0.8rem; }

    /* Seen is a flag that LIGHTS: hollow when unseen, filled gold when seen.
       No checkbox — a checkbox here would collide with the t-shirt selection
       control that is coming, and read as the same kind of thing. */
    .log-flag.on         { color: var(--gold); }
    .log-del:hover       { border-color: var(--red); color: var(--red); }
    .log-goto:hover      { color: var(--text); }

    /* Grid, not wrapped flex: four equal icon buttons on one row. It was five
       text buttons in a 2-column grid with "Make map" spanning both — wrapped
       flex before that, which left Import stranded alone because wrapping
       depends on text width rather than on any intent. Grid keeps the intent
       explicit at any label length, which is why it stays now the labels are
       gone. */
    .log-tools {
      display:               grid;
      grid-template-columns: repeat(4, 1fr);
      gap:                   0.4rem;
      padding:               0.85rem 0.75rem;
      border-top:            1px solid var(--border);
    }
    /* The base rule. It was lost in a rewrite of this block, which left every
       button in the panel as a bare <button> — browser-default grey-on-silver.
       If these ever look like OS buttons again, this rule has gone missing. */
    .log-btn {
      font-family:    var(--mono);
      font-size:      0.72rem;
      letter-spacing: 0.06em;
      padding:        0.4rem 0.5rem;
      cursor:         pointer;
      background:     transparent;
      border:         1px solid var(--border);
      border-radius:  var(--radius);
      color:          var(--text-dim);
      white-space:    nowrap;
      text-align:     center;
    }
    .log-btn:hover { border-color: var(--edge); color: var(--text); }
    /* Icon-only now, so the button is a box for one 20px mark rather than a run
       of text: centre it and let the padding set the hit area, which stays
       comfortably past the 44px touch minimum at this padding. */
    .log-btn svg {
      width:   20px;
      height:  20px;
      display: block;
      margin:  0 auto;
    }


    /* ── Sheet ────────────────────────────────────────────────────────
       A reusable overlay. The three map overlays are queued to use this same
       pattern, so dismissal, backdrop and z-index are settled once here.
       Slides up on mobile, centres on desktop. */
    .sheet {
      position: fixed;
      top:    0; right: 0; bottom: 0; left: 0;
      z-index:  1100;
      display:  none;
    }
    .sheet.open { display: block; }
    .sheet-backdrop {
      position:   absolute;
      top:      0; right: 0; bottom: 0; left: 0;
      background: rgba(0,0,0,0.72);
    }
    .sheet-body {
      position:       absolute;
      left:           0;
      right:          0;
      bottom:         0;
      /* DEFINITE height, not just a max. .sheet-content is flex:1 1 0, so it
         contributes nothing to intrinsic height — with max-height alone the
         whole sheet collapses to head + foot and the map disappears. The two
         must be changed together. */
      height:         88vh;
      max-height:     92vh;
      display:        flex;
      flex-direction: column;
      background:     var(--bg2);
      border-top:     1px solid var(--border2);
      border-radius:  var(--radius) var(--radius) 0 0;
      transform:      translateY(100%);
      transition:     transform 0.22s ease;
    }
    .sheet.open .sheet-body { transform: translateY(0); }

    .sheet-head, .sheet-foot {
      display:     flex;
      align-items: center;
      gap:         0.5rem;
      padding:     0.7rem 0.85rem;
      flex:        0 0 auto;
    }
    .sheet-head { border-bottom: 1px solid var(--border); }
    .sheet-foot { border-top:    1px solid var(--border); flex-wrap: wrap; }
    .sheet-title {
      font-family:    var(--mono);
      font-size:      0.8rem;
      font-weight:    700;
      letter-spacing: 0.15em;
      text-transform: uppercase;
      color:          var(--gold);
    }
    .sheet-sub {
      font-family: var(--mono);
      font-size:   0.7rem;
      color:       var(--text-dim);
    }
    /* A proper tap target. At 1.3rem of glyph with 0.2rem of padding this was
       well under the 44px minimum, and on a phone it is the main way out — the
       backdrop is only the top ~12% of the screen. */
    /* A LABELLED button, not a bare glyph. A lone × in the corner of a sheet
       was reported as "no indication of how to escape" — the word is what makes
       it read as a way out. Full 44px tap target, bordered so it reads as a
       control rather than decoration. */
    .sheet-close {
      margin-left:     auto;
      display:         flex;
      align-items:     center;
      justify-content: center;
      gap:             0.1rem;
      min-height:      2.4rem;
      padding:         0 0.7rem;
      background:      transparent;
      border:          1px solid var(--edge);
      border-radius:   var(--radius);
      color:           var(--text);
      font-family:     var(--mono);
      font-size:       0.78rem;
      letter-spacing:  0.06em;
      line-height:     1;
      cursor:          pointer;
      flex:            0 0 auto;
    }
    .sheet-close:hover { color: var(--text-pale); }

    /* Grab handle: the standard signal that a sheet can be pulled down. Mobile
       only — on desktop this is a centred dialog with Escape and a backdrop. */
    .sheet-body::before {
      content:       '';
      display:       block;
      width:         2.5rem;
      height:        0.25rem;
      margin:        0.45rem auto 0;
      border-radius: 999px;
      background:    var(--border2);
      flex:          0 0 auto;
    }
    @media (min-width: 900px) {
      .sheet-body::before { display: none; }
    }

    .sheet-content {
      flex:            1 1 0;
      overflow:        hidden;   /* the map is scaled to fit, never scrolled */
      padding:         0.85rem;
      min-height:      0;        /* lets a flex child actually shrink */
      display:         flex;
      align-items:     center;
      justify-content: center;
    }
    /* Fit the poster INSIDE the sheet in both axes. width:100% alone overflows
       any projection taller than it is wide, which is why the maps ran off the
       bottom; max-height plus the SVG's own preserveAspectRatio letterboxes it
       instead. */
    .sheet-content svg {
      display:    block;
      width:      auto;
      height:     auto;
      max-width:  100%;
      max-height: 100%;
    }
    .sheet-foot select { width: auto; min-width: 9rem; font-size: 0.72rem; }
    .ts-wait {
      font-family: var(--mono);
      font-size:   0.78rem;
      color:       var(--text-dim);
      text-align:  center;
      padding:     2rem 1rem;
      line-height: 1.7;
    }

    @media (min-width: 900px) {
      /* Centred dialog on desktop — a bottom sheet on a wide screen wastes the
         width the poster actually needs. */
      .sheet-body {
        top:            50%;
        left:           50%;
        right:          auto;
        bottom:         auto;
        width:          min(980px, 92vw);
        height:         86vh;
        max-height:     86vh;
        border:         1px solid var(--border2);
        border-radius:  var(--radius);
        transform:      translate(-50%, -46%);
        opacity:        0;
        transition:     transform 0.18s ease, opacity 0.18s ease;
      }
      .sheet.open .sheet-body { transform: translate(-50%, -50%); opacity: 1; }
    }

    /* The reserved column, now used: t-shirt selection. */
    .log-pick {
      grid-area:  pick;
      align-self: start;
      margin:     0.3rem 0 0;
      accent-color: var(--gold);
      cursor:     pointer;
      flex:       0 0 auto;
    }

    .share-modal-backdrop {
      display: none;
      position: fixed; top: 0; right: 0; bottom: 0; left: 0;
      background: rgba(0,0,0,0.75);
      z-index: 1000;
      align-items: center;
      justify-content: center;
    }
    .share-modal-backdrop.open { display: flex; }
    .share-modal {
      background: var(--bg3);
      border: 1px solid var(--border2);
      border-radius: 6px;
      padding: 1.25rem 1.5rem 1.5rem;
      max-width: 460px;
      width: 92%;
      box-shadow: 0 8px 32px rgba(0,0,0,0.6);
    }
    .share-modal-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      margin-bottom: 1rem;
    }
    .share-modal-title {
      font-family: var(--mono);
      font-size: 0.72rem;
      letter-spacing: 0.12em;
      text-transform: uppercase;
      color: var(--text);
    }
    .share-modal-close {
      background: none; border: none; cursor: pointer;
      color: var(--text-dim); font-size: 1.2rem; line-height: 1;
      padding: 0 0.2rem;
    }
    .share-modal-close:hover { color: var(--text); }
    .share-modal textarea {
      width: 100%; box-sizing: border-box;
      background: var(--bg); border: 1px solid var(--border);
      border-radius: var(--radius);
      color: var(--text); font-family: var(--mono); font-size: 0.72rem;
      line-height: 1.6;
      padding: 0.75rem; resize: none; height: 180px;
    }
    .share-modal-btns { display: flex; gap: 0.5rem; margin-top: 0.75rem; justify-content: flex-end; }

    /* Inline data panel below eclipse list */
    #data-panel .panel {
      background:    transparent;
      border:        none;
      border-radius: 0;
      margin-bottom: 0;
      padding:       0;
    }


    .partial-slider-wrap {
      display:     flex;
      align-items: center;
      gap:         0.35rem;
      flex-shrink: 0;
    }
    .partial-slider-wrap input[type=range] {
      width:   5rem;
      padding: 0;
      border:  none;
      background: transparent;
      accent-color: var(--text-dim);
    }
    .partial-min-label {
      font-family: var(--mono);
      font-size:   0.65rem;
      color:       var(--text-dim);
      white-space: nowrap;
      min-width:   2.5rem;
    }

    .pill.pill-today {
      font-style:   italic;
      color:        var(--text-dim);
      border-color: var(--edge);
    }
    .pill.pill-today.active {
      background:   rgba(200,169,110,0.12);
      border-color: var(--edge);
      color:        var(--gold);
    }


    /* ── Map tab ─────────────────────────────────────────────────────── */

    #map {
      position: absolute;
      top:    0; right: 0; bottom: 0; left: 0;
      width:    100%;
      height:   100%;
    }

    /* Terrain-shadow toggle — top-left overlay button */
    /* ── On-map controls ──────────────────────────────────────────────────
       Overlays (toggles, independent) sit top LEFT; the basemap choice
       (exclusive, one of three) sits top RIGHT. Opposite corners so the two
       kinds of control are never mistaken for one another, and the bottom edge
       stays clear for the scrubber and the tab bar. */
    .map-overlay-btns {
      position: absolute;
      top:      0.6rem;
      left:     0.6rem;
      z-index:  200;
      display:  flex;
      gap:      0.4rem;
    }
    /* Icon-only. The glyphs carry the meaning: a hard-edged half-lit circle
       (terrain shadow) beside a soft cloud outline — different silhouettes, so
       they separate at a glance where two words would just be two words.

       ALL THREE ARE SVG, and the Instructions quote the SAME markup with a
       different class. They were Unicode characters until 2026-09-13 and could
       never be made to match: a symbol character has no guaranteed size or
       baseline relative to another (SS14), so the cloud drew as a melted lump
       beside a crisp circle whatever font-size was applied. SS11.9 had already
       recorded this and swapped the PROSE to SVG; leaving the buttons as text
       meant prose and button still disagreed. Now there is one set of paths,
       in one box, so they cannot drift again. */
    .map-overlay-btn .btn-icon { width: 1.15rem; height: 1.15rem; display: block; }

    .map-overlay-btn {
      width:           2.1rem;
      height:          2.1rem;
      display:         flex;
      align-items:     center;
      justify-content: center;
      background:      rgba(10,12,15,0.88);
      border:          1px solid var(--border2);
      border-radius:   var(--radius);
      color:           var(--text-dim);
      font-size:       1.15rem;
      line-height:     1;
      cursor:          pointer;
      backdrop-filter: blur(4px);
      pointer-events:  auto;
    }
    .map-overlay-btn:hover                { border-color: var(--edge); color: var(--text); }
    .map-overlay-btn[aria-pressed="true"] { border-color: var(--gold); color: var(--gold); }
    .map-overlay-btn:disabled,
    .map-overlay-btn[aria-disabled="true"] { opacity: 0.4; cursor: not-allowed; }

    /* ── Cloud caption strip ──────────────────────────────────────────────
       Sits directly under the overlay buttons, so the legend is adjacent to the
       control that summoned it. Chrome matches the basemap picker's — same
       translucent slab, same border and blur — because it is the same KIND of
       thing: an on-map control panel. Widths are capped rather than fixed so a
       long satellite name wraps instead of pushing the strip off a phone. */
    #cloudbar,
    #favorbar {
      position:        absolute;
      top:             3.2rem;
      left:            0.6rem;
      z-index:         200;
      /* Was 17rem — sized for the widest text label ('Average'). Icon-only
         buttons need much less, and a narrower box is directly the "less
         space on mobile" ask. */
      /* width, NOT max-width. Absolutely positioned, so max-width let the box
         shrink-to-fit and it came out a different WIDTH in each mode once the
         credit strings differed in length. Pinned, it cannot. */
      width:           min(12rem, calc(100vw - 1.6rem));
      padding:         0.4rem;
      display:         flex;
      flex-direction:  column;
      gap:             0.35rem;
      background:      rgba(10,12,15,0.88);
      border:          1px solid var(--border2);
      border-radius:   var(--radius);
      backdrop-filter: blur(4px);
      pointer-events:  auto;
    }
    #cloudbar[hidden], #favorbar[hidden] { display: none; }
    /* Phones: the favorability box keeps only its two buttons and the colour
       scale. The explanatory note costs map space; the Instructions carry it. */
    @media (max-width: 899px) {
      #favorbar .cloudbar-info { display: none; }
    }

    /* The mode switch. A segmented control, deliberately the same idiom as the
       basemap picker: one of N, mutually exclusive, current state shown by the
       active cell rather than by a separate indicator. */
    /* Two-level strip: `Average` beside a capped `Now` group holding Map and Pic.
       `align-items: stretch` is what makes Average full height without a
       hardcoded height that would drift from the cells' padding. */
    .cloudbar-modes {
      display:     flex;
      gap:         0.25rem;
      align-items: stretch;
    }
    .cloudbar-cell {
      flex:            1;
      display:         flex;
      align-items:     center;
      justify-content: center;
      /* Fixed square-ish footprint, icon-only now — the old text labels
         ('Average' vs 'Map') were different widths, which is most of why the
         group used to visibly resize. A number, not 'auto', is what makes
         that stop: every cell claims the same space regardless of icon. */
      padding:         0.35rem 0;
      color:           var(--text-dim);
      background:      transparent;
      border:          1px solid var(--border2);
      border-radius:   calc(var(--radius) - 2px);
      cursor:          pointer;
    }
    .cloudbar-cell:hover:not(:disabled) { color: var(--text); border-color: var(--edge); }
    .cloudbar-cell.active { color: var(--gold); border-color: var(--gold); }
    /* Unavailable, not broken: the cell keeps its place and carries a title
       explaining why, exactly as the shadow button does when offline. */
    .cloudbar-cell:disabled { opacity: 0.35; cursor: not-allowed; }

    /* Reserves height for the taller of the two caption states (Map/Pic's
       three lines vs Average's two) so switching modes doesn't grow or shrink
       the box. min-height, not height — a genuinely longer satellite-missing
       line still gets to wrap without clipping. */
    .cloudbar-info { min-height: 2.7rem; }

    .cloudbar-group {
      flex:           2;      /* two sub-cells, so twice Average's share */
      display:        flex;
      flex-direction: column;
      gap:            2px;
      min-width:      0;      /* or the nowrap sub-labels push the row wide */
    }
    .cloudbar-cap {
      font-size:      0.62rem;
      letter-spacing: 0.08em;
      text-transform: uppercase;
      text-align:     center;
      padding:        1px 0;
      color:          var(--text-dim);
      border:         1px solid var(--border2);
      border-bottom:  none;
      border-radius:  calc(var(--radius) - 2px) calc(var(--radius) - 2px) 0 0;
    }
    .cloudbar-cap.active { color: var(--gold); border-color: var(--gold); }
    .cloudbar-subs { display: flex; gap: 2px; }
    /* Square off the edges the cap sits on, so cap and sub-cells read as one
       object rather than a label that happens to be above two buttons. */
    .cloudbar-sub    { min-width: 0; padding-left: 0.3rem; padding-right: 0.3rem; }
    .cloudbar-sub-l  { border-radius: 0 0 0 calc(var(--radius) - 2px); }
    .cloudbar-sub-r  { border-radius: 0 0 calc(var(--radius) - 2px) 0; }
    /* Offline greys the WHOLE group at once. The cell rule already dims each
       button; dimming the cap here is what makes it read as one unavailable
       thing carrying one reason, rather than two broken buttons. */
    .cloudbar-group.blocked .cloudbar-cap { opacity: 0.35; cursor: not-allowed; }

    /* The colour bar. Height is deliberately small — it is a key, not a chart —
       and the end labels are words rather than numbers because the underlying
       quantity is a mean fraction, which a percentage scale would make look more
       exact than it is. */
    .cloudbar-legend {
      display:     flex;
      align-items: center;
      gap:         0.3rem;
    }
    .cloudbar-ramp {
      flex:          1;
      height:        0.45rem;
      border-radius: 2px;
      border:        1px solid var(--border2);
    }
    .cloudbar-end {
      font-size:   0.62rem;
      color:       var(--text-dim);
      white-space: nowrap;
    }
    .cloudbar-note {
      font-size:   0.65rem;
      line-height: 1.35;
      color:       var(--text-dim);
    }
    .cloudbar-credit { opacity: 0.7; }

    /* Small spinner shown to the left of "loading…" in the age line while
       waiting for the first frame. A CSS-only ring, no image asset. */
    .cloudbar-spinner {
      display:        inline-block;
      width:          0.55rem;
      height:         0.55rem;
      margin-right:   0.3rem;
      vertical-align: -1px;
      border:         1.5px solid var(--border2);
      border-top-color: var(--gold);
      border-radius:  50%;
      animation:      cloudbar-spin 0.7s linear infinite;
    }
    @keyframes cloudbar-spin {
      to { transform: rotate(360deg); }
    }

    @media (max-width: 900px) {
      #cloudbar { top: 2.9rem; max-width: min(10rem, calc(100vw - 1.6rem)); padding: 0.3rem; }
      .cloudbar-cap { font-size: 0.58rem; }
    }

    /* Right-hand map controls: basemap picker + locate pin, one row.
       The wrapper owns the anchoring so neither child positions itself — they
       used to share top/right 0.6rem and z-index 200, which meant the picker
       (later in the DOM) painted over the pin on every viewport. */
    .map-right-controls {
      position:    absolute;
      top:         0.6rem;
      right:       0.6rem;
      z-index:     200;
      display:     flex;
      align-items: flex-start;
      gap:         0.4rem;
    }

    /* Basemap picker: three small drawn map fragments, no words. Live provider
       tiles were tried first — at 44px a zoomed-out topo tile is
       indistinguishable from a street one. The swatches are inline SVG (see
       PICKER_SWATCH in js/map.js), so nothing is fetched: no pop-in, and the
       control looks the same offline as on. */
    .basemap-picker {
      display:  inline-flex;
      gap:      0.3rem;
      padding:  0.3rem;
      background:      rgba(10,12,15,0.88);
      border:          1px solid var(--border2);
      border-radius:   var(--radius);
      backdrop-filter: blur(4px);
      pointer-events:  auto;
    }

    /* The pin matches the picker's chrome and its tiles' footprint, so the two
       read as one row of controls rather than two unrelated widgets. */
    #btn-locate {
      display:         flex;
      align-items:     center;
      justify-content: center;
      width:           3.2rem;
      height:          3.2rem;
      padding:         0;
      font-size:       1.3rem;
      line-height:     1;
      cursor:          pointer;
      background:      rgba(10,12,15,0.88);
      border:          1px solid var(--border2);
      border-radius:   var(--radius);
      backdrop-filter: blur(4px);
      pointer-events:  auto;
    }
    #btn-locate:hover { border-color: var(--edge); }
    .basemap-picker.is-offline { opacity: 0.4; }
    .basemap-opt {
      width:         2.6rem;
      height:        2.6rem;
      padding:       0;
      background:    none;
      /* Transparent on every tile so the active one doesn't resize when it
         lights up — the reserved width must match .active's. Change both or
         neither. 1px reads as a hairline at any density; the old 1.5px plus an
         outer glow made a band rather than a ring. */
      border:        1px solid transparent;
      border-radius: 5px;
      cursor:        pointer;
      overflow:      hidden;
      opacity:       0.72;
      transition:    opacity 0.12s, border-color 0.12s;
    }
    .basemap-swatch, .basemap-swatch svg {
      display: block;
      width:   100%;
      height:  100%;
    }
    .basemap-swatch { border-radius: 3.5px; overflow: hidden; }
    .basemap-opt:hover    { opacity: 1; border-color: var(--edge); }
    /* The active one is simply the bright, ringed one — no tick, no label.
       The 1px inset shadow is a dark gap between swatch and ring, so the gold
       reads as a ring around the tile rather than a bright edge on it — the
       swatch is flush to the border under overflow:hidden, and without the gap
       a pale basemap and the gold blend into one another. */
    .basemap-opt.active   { opacity: 1; border-color: var(--gold);
                            box-shadow: inset 0 0 0 1px rgba(10,12,15,0.75); }
    .basemap-opt:disabled { cursor: not-allowed; }

    /* Phones: shrink rather than hide — the picker is the only way to change
       basemap now that the Settings pulldown is gone. */
    @media (max-width: 899px) {
      /* Collapsed picker: only the ACTIVE tile shows, so the control still
         reports which basemap is live while taking one tile of width instead of
         three. Tapping it adds .expanded (see renderBasemapPicker in map.js). */
      /* Slide, don't blink: the hidden tiles keep their box and collapse their
         width, so opening the picker reads as one control widening rather than
         two more appearing from nowhere. `display:none` cannot be animated,
         which is why width/opacity carry it instead. */
      .basemap-picker .basemap-opt {
        transition: width 0.18s ease, opacity 0.14s ease,
                    margin 0.18s ease, border-color 0.12s;
      }
      .basemap-picker:not(.expanded) .basemap-opt:not(.active) {
        width:         0;
        opacity:       0;
        margin-left:   -0.3rem;   /* swallow the flex gap while collapsed */
        border-width:  0;
        pointer-events: none;
      }

      /* Everything a notch smaller here — three clusters across a 360px phone
         needs the room more than it needs the tap target. */
      .basemap-opt    { width: 1.95rem; height: 1.95rem; }
      .map-overlay-btn { width: 1.9rem; height: 1.9rem; font-size: 1.05rem; }
      .map-overlay-btn .btn-icon { width: 1.05rem; height: 1.05rem; }
      /* Matches the picker chip's height: tile + its 2 x 0.3rem padding + borders. */
      #btn-locate     { width: 2.7rem; height: 2.7rem; font-size: 1.1rem; }
    }

    /* Terrain-shadow time scrubber — flush full-width bar at the bottom. Left:
       selected date/time on two lines. Right: a ruler whose strip slides past a
       fixed centre needle. Shown only while shadows are on. */
    .shadow-timeline {
      position:        absolute;
      left:            0;
      right:           0;
      /* The PANEL runs to the very bottom of the screen so there is no stripe
         of map showing beneath it; the CONTROLS are lifted clear of the iOS
         home indicator by padding instead. At bottom:0 with no padding the
         scrubber sat in the system's swipe-up gesture zone and dragging it
         switched apps. viewport-fit=cover is already set on the viewport meta,
         so env() reports the real inset, and the 0px fallback leaves every
         other platform pixel-identical. */
      bottom:          0;
      padding-bottom:  env(safe-area-inset-bottom, 0px);
      box-sizing:      content-box;
      z-index:         20;
      display:         flex;
      align-items:     stretch;
      height:          34px;
      background:      rgba(10,12,15,0.92);
      border-top:      1px solid var(--border2);
      overflow:        hidden;
      backdrop-filter: blur(4px);
    }
    .shadow-timeline[hidden] { display: none; }
    /* Installed PWA on a device with a home indicator: a little extra clearance
       so the SCRUBBER TRACK itself, not just the panel edge, is out of the
       gesture zone. Browsers ignore this (display-mode: browser). */
    @media all and (display-mode: standalone) {
      /* A little extra so the scrubber TRACK, not just the panel edge, is out
         of the gesture zone. Browsers ignore this (display-mode: browser). */
      .shadow-timeline { padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 8px); }
    }

    .shadow-readout {
      flex:          0 0 auto;
      display:       flex;
      flex-direction: column;
      justify-content: center;
      padding:       0 0.7rem;
      border-right:  1px solid var(--border2);
      font-family:   var(--mono);
      line-height:   1.15;
      user-select:   none;
      -webkit-user-select: none;
    }
    /* Fixed width, or the ruler slides sideways under the static needle whenever
       the text changes length ("local" is longer than "UTC"). Sized in ch — the
       stack is genuinely monospace, so 1ch is one character. Both lines max out
       at 11: "31 Dec 2026" and "14:32 local". Per-child min-width (rather than a
       width on the parent) keeps this correct if either font-size changes. */
    .shadow-date { font-size: 0.6rem;  color: var(--text-dim); white-space: nowrap; min-width: 11ch; }
    .shadow-clock{ font-size: 0.72rem; color: var(--gold2);     white-space: nowrap; min-width: 11ch; }

    .shadow-ruler-wrap {
      position:    relative;
      flex:        1 1 auto;
      height:      100%;
      overflow:    hidden;
      cursor:      ew-resize;
      touch-action: none;
      outline:     none;
      user-select: none;
      -webkit-user-select: none;
    }
    .shadow-ruler {
      position:    absolute;
      top:         0;
      left:        0;
      height:      100%;
      will-change: transform;
    }
    .shadow-tick {
      position:   absolute;
      bottom:     0;
      width:      1px;
      height:     5px;
      background: var(--border2);
    }
    .shadow-tick.major { height: 9px; background: var(--text-dim); }
    .shadow-tick-label {
      position:    absolute;
      bottom:      11px;
      transform:   translateX(-50%);
      font-family: var(--mono);
      font-size:   0.6rem;
      color:       var(--text-dim);
      white-space: nowrap;
    }
    .shadow-center {
      position:      absolute;
      left:          50%;
      top:           0;
      bottom:        0;
      width:         0;
      border-left:   2px solid var(--gold);
      pointer-events: none;
    }
    .shadow-center::after {
      content:      '';
      position:     absolute;
      top:          0;
      left:         -5px;
      border-left:  5px solid transparent;
      border-right: 5px solid transparent;
      border-top:   6px solid var(--gold);
    }

    /* Zoomed-out hint replaces the readout + ruler */
    .shadow-hint {
      display:     none;
      flex:        1;
      align-items: center;
      justify-content: center;
      font-family: var(--mono);
      font-size:   0.68rem;
      color:       var(--text-dim);
      user-select: none;
      -webkit-user-select: none;
    }
    .shadow-timeline.hint .shadow-readout,
    .shadow-timeline.hint .shadow-ruler-wrap { display: none; }
    .shadow-timeline.hint .shadow-hint       { display: flex; }

    /* MapLibre attribution — restyle to match dark theme */
    .maplibregl-ctrl-attrib {
      background: rgba(10,12,15,0.8) !important;
      color:      var(--text-dim) !important;
      font-size:  0.6rem !important;
    }
    .maplibregl-ctrl-attrib a { color: var(--text-dim) !important; }

    /* On phones the expanded attribution eats a whole strip of the map. MapLibre
       already ships a collapsed mode — the compact class plus its (i) toggle —
       but only applies it at its own breakpoint. Force it below 900px: the text
       hides behind the (i) and reappears when tapped, so credit is still one tap
       away and nothing is removed. Desktop keeps the inline text. */
    @media (max-width: 899px) {
      .maplibregl-ctrl-attrib.maplibregl-compact {
        min-height:    24px;
        padding:       0;
        margin:        0 8px 8px 0;
        border-radius: 12px;
      }
      .maplibregl-ctrl-attrib.maplibregl-compact:not(.maplibregl-compact-show)
        .maplibregl-ctrl-attrib-inner { display: none; }
      .maplibregl-ctrl-attrib.maplibregl-compact-show
        .maplibregl-ctrl-attrib-inner { display: block; }
    }

    /* Click-result popup */
    .map-popup {
      position:      absolute;
      bottom:        0.75rem;
      left:          0.75rem;
      z-index:       10;
      background:    rgba(10,12,15,0.92);
      border:        1px solid var(--border2);
      border-radius: var(--radius);
      padding:       0.75rem 1rem;
      font-family:   var(--mono);
      font-size:     0.72rem;
      display:       none;
      backdrop-filter: blur(4px);
      width:         max-content;
      max-width:     min(340px, calc(100vw - 1.5rem));
    }
    .map-popup-close {
      float:       right;
      background:  none;
      border:      none;
      color:       var(--text-dim);
      cursor:      pointer;
      font-size:   1rem;
      line-height: 1;
      padding:     0;
      margin-left: 0.5rem;
    }
    .map-popup-close:hover { color: var(--text); }
    .map-popup-title {
      color:         var(--text);
      margin-bottom: 0.4rem;
    }
    .map-popup-grid {
      display:               grid;
      grid-template-columns: 1fr 1fr;
      gap:                   0.2rem 1rem;
      color:                 var(--text);
    }
    .map-popup-label { color: var(--text-dim); font-size: 0.65rem; }

    /* Sun direction indicator on observer marker.
       The observer is a DRAWN PIN (see pinImage() in js/map.js) whose TIP is the
       coordinate; the arrow is a separate co-located marker that rotates about
       that same point. (.observer-dot / .ge-dot retired with the Cesium build.) */
    /* deck.gl's overlay is added with map.addControl(), so its canvas lives in a
       .maplibregl-ctrl-* corner container — and MapLibre gives those z-index:2
       while .maplibregl-marker gets none. Without this the eclipse paths draw
       OVER the pin, arrow and GE diamond. Markers belong on top of the lines. */
    .maplibregl-marker {
      z-index: 3;
    }
    /* The observer's pin and sun arrow outrank every other marker (the GE
       diamond). With equal z-index the later-added marker wins, and the order
       differs by route: a hand-placed pin comes AFTER the diamond, but on a
       reload with a pin in the URL the pin is added first and the diamond then
       covered it. Two-class selectors so this outranks the rule above. */
    .maplibregl-marker.observer-pin-wrap,
    .maplibregl-marker.sun-arrow-wrap {
      z-index: 4;
    }
    /* Same rule as .observer-pin-wrap: no `position` declaration. The absolute
       positioning inherited from .maplibregl-marker is itself the containing
       block .sun-arrow needs, and it keeps the wrapper out of normal flow. */
    /* Separates the map-overlays block at the end of Instructions. A rule read
       as a hard structural break; an ornament reads as a breath. Sun, moon and
       star — dimmed and letter-spaced so it decorates rather than announces. */
    .instructions-ornament {
      margin:         1.4rem 0 1.1rem;
      text-align:     center;
      color:          var(--gold);
      font-size:      0.95rem;
      letter-spacing: 0.35em;
      line-height:    1;
      user-select:    none;
    }

    .sun-arrow-wrap {
      width:  44px;
      height: 44px;
    }
    /* The wrapper is the MARKER element (MapLibre owns its transform); the
       image inside carries the zoom scale. Size comes from PIN_W/PIN_H in
       js/map.js, which also draw the artwork — one source, no drift.
       Do NOT set `position` here: .maplibregl-marker is position:absolute, and
       overriding it to relative drops the wrapper into normal flow, where a
       preceding in-flow marker shifts it off the coordinate. */
    .observer-pin-wrap {
      pointer-events: none;
    }
    .observer-pin {
      display:           block;
      max-width:         none;
      max-height:        none;
      pointer-events:    none;
      user-select:       none;
      -webkit-user-drag: none;
      filter: drop-shadow(0 1px 2px rgba(0,0,0,0.35));
    }
    /* Cesium proportions: total 44px, head 30% of length (13.2px),
       head half-width 0.42 x head length (5.5px); stem is the remaining 70%. */
    .sun-arrow {
      position:         absolute;
      top:              50%;
      left:             50%;
      width:            30.8px;
      height:           2.6px;
      background:       var(--pin-red);
      transform-origin: 0 50%;
      opacity:          0.9;
    }
    .sun-arrow::after {
      content:       '';
      position:      absolute;
      right:         -13.2px;
      top:           -4.2px;
      border-left:   13.2px solid var(--pin-red);
      border-top:    5.5px solid transparent;
      border-bottom: 5.5px solid transparent;
    }
    /* Greatest eclipse — orange diamond, deliberately distinct from the red observer. */
    .ge-diamond {
      width:      12px;
      height:     12px;
      background: #f08a1e;
      border:     1.5px solid rgba(255,255,255,0.92);
      transform:  rotate(45deg);
      box-shadow: 0 0 6px rgba(240,138,30,0.55);
    }
    /* The same two marks, small, before their Global Circumstances labels
       (details.js GE_MARK / GD_MARK). No white edge: it vanishes in light mode. */
    .gc-mark {
      display:        inline-block;
      box-sizing:     border-box;
      margin-right:   0.45em;
      vertical-align: 0.05em;
    }
    .gc-mark-ge {
      width:      0.55em;
      height:     0.55em;
      background: #f08a1e;
      transform:  rotate(45deg);
    }
    .gc-mark-gd {
      width:         0.8em;
      height:        0.8em;
      border:        0.17em solid #f08a1e;
      border-radius: 50%;
      vertical-align: -0.05em;
    }
    /* Greatest duration — the diamond's orange, hollow (map.js addGDMarker). */
    .gd-ring {
      width:         14px;
      height:        14px;
      box-sizing:    border-box;
      border:        3px solid #f08a1e;
      border-radius: 50%;
      box-shadow:    0 0 0 1.5px rgba(255,255,255,0.92), 0 0 6px rgba(240,138,30,0.55);
    }


    /* ── Responsive ──────────────────────────────────────────────────── */

    @media (max-width: 600px) {
      /* Hide the branding banner on mobile browser — screen space is precious and
         Safari's own chrome already eats the top, so the tab bar becomes the top
         edge. --header-h:0 lets the panel max-height calc reclaim the space.
         (In the installed PWA the banner is already hidden via display-mode.) */
      :root { --header-h: 0rem; }
      .app-header   { display: none; }
      .tab-btn      { font-size: 1.1rem; letter-spacing: 0; }
      .tab-label    { display: none; }
      .tab-panel    { padding: 0.75rem 0.75rem 6rem; --panel-pad-x: 0.75rem; }

      /* Larger base font for readability on phone */
      body { font-size: 1.05rem; }
      .eclipse-item { font-size: 1rem; padding: 0.75rem; }
      .search-hint  { font-size: 0.78rem; }
      .pill         { font-size: 0.8rem; }
      .section-label { font-size: 0.85rem; }
      .top-section-header, .detail-section-h { font-size: 0.95rem; }
      .circ-row .l        { font-size: 0.72rem; }
      .circ-row .v        { font-size: 0.95rem; }
      .circ-row           { padding: 0.4rem 0; }

      /* Hide map popup on mobile — it renders off-screen */
      .map-popup { display: none !important; }
    }


    /* ── Sidebar sub-tab buttons (DOM-present on all viewports;
          visually hidden on mobile, shown on desktop) ─────────────────── */

    .sidebar-tabs { display: none; }
    .sidebar-resize-handle { display: none; }

    .sidebar-tab-btn {
      flex:           1;
      background:     var(--bg2);
      color:          var(--text-dim);
      border:         1px solid var(--border);
      border-bottom:  none;
      border-radius:  var(--radius) var(--radius) 0 0;
      margin-right:   -1px;
      padding:        0.45rem 0.35rem 0.5rem;
      font-family:    var(--mono);
      font-size:      0.72rem;
      letter-spacing: 0.04em;
      cursor:         pointer;
      white-space:    nowrap;
      position:       relative;
    }
    .sidebar-tab-btn:last-child { margin-right: 0; }
    .sidebar-tab-btn:hover      { color: var(--text); }
    .sidebar-tab-btn.active {
      color:         var(--gold);
      background:    #1a2030;
      margin-bottom: -1px;
      z-index:       2;
    }


    /* ── Desktop layout: two-column grid with right-side sidebar ─────── */

    @media (min-width: 900px) {

      /* No GPS on a desktop — browser geolocation there resolves by IP or
         Wi-Fi and lands you in the wrong town, which is worse than useless for
         an app whose whole point is where the shadow falls to the kilometre.
         Click the map or type coordinates instead. The startup geolocation
         request in js/init.js is unaffected; this hides only the manual
         re-trigger. */
      #btn-locate { display: none; }

      /* Two-column flex inside .tab-content: map flexes, sidebar fixed-width.
         position:relative anchors .sidebar-resize-handle (a sibling of
         .sidebar, positioned in tab-content's coordinate space so it can
         straddle the sidebar's edge without being clipped by the sidebar's
         own overflow:hidden). */
      .tab-content {
        display:        flex;
        flex-direction: row;
        position:       relative;
      }

      /* Bottom tab-bar (mobile only) is hidden on desktop */
      .tab-bar { display: none; }

      /* Map: always visible, dominant left column */
      #tab-map {
        position: relative;
        top:    auto; right: auto; bottom: auto; left: auto;
        flex:     1 1 auto;
        display:  block;
        padding:  0;
      }

      /* Sidebar: right column with sub-tabs on top. Width is a CSS var so the
         drag handle (below) can resize it; 360px is both the default and the
         floor — set as min-width too, so the var can never shrink the layout
         past what a fixed-width sidebar used to guarantee. */
      :root { --sidebar-w: 360px; }
      .sidebar {
        display:        flex;
        flex-direction: column;
        flex:           0 0 var(--sidebar-w);
        min-width:      360px;
        max-width:      720px;
        border-left:    1px solid var(--border);
        background:     #1a2030;
        overflow:       hidden;
        position:       relative;   /* anchors .sidebar-resize-handle */
      }

      /* Discreet drag handle on the sidebar's left edge. Invisible at rest —
         only the border-left already there marks the edge — and lights up
         gold on hover/drag so it reads as interactive without adding a
         permanent visual seam. */
      .sidebar-resize-handle {
        display:  block;
        position: absolute;
        /* Positioned from the right so it tracks the sidebar's actual left
           edge as --sidebar-w changes (this element lives in .tab-content's
           coordinate space, not .sidebar's, so it isn't clipped by the
           sidebar's overflow:hidden). ±4px around that edge, same straddle
           as before. */
        right:    calc(var(--sidebar-w) - 4px);
        top:      0;
        bottom:   0;
        width:    8px;
        cursor:   ew-resize;
        z-index:  6;
        touch-action: none;
      }
      .sidebar-resize-handle::after {
        content:      '';
        position:     absolute;
        left:         2px;
        top:          50%;
        width:        4px;
        height:       36px;
        transform:    translateY(-50%);
        border-radius: 2px;
        background:   var(--text-dim);
        opacity:      0.5;
      }
      .sidebar-resize-handle:hover::after,
      .sidebar-resize-handle.dragging::after {
        background: var(--gold);
        opacity:    1;
      }
      .sidebar-tabs {
        display:        flex;
        flex-direction: row;
        padding:        0.5rem 0.5rem 0;
        background:     #040508;
        border-bottom:  1px solid var(--border);
      }

      /* Panels inside sidebar: one shown at a time based on data-sidebar-tab. */
      .sidebar .tab-panel {
        position:  relative;
        top:     auto; right: auto; bottom: auto; left: auto;
        flex:      1 1 auto;
        display:   none;
        padding:   0.75rem 1rem 1rem;
        --panel-pad-x: 1rem;
      }
      .sidebar .tab-panel > * { max-width: none; margin: 0; }

      body[data-sidebar-tab="search"]   #tab-search,
      body[data-sidebar-tab="eclipse"]  #tab-eclipse,
      body[data-sidebar-tab="log"]      #tab-log,
      body[data-sidebar-tab="settings"] #tab-settings {
        display: block;
      }

      /* Hide the overlay whose information now lives in the sidebar:
         - #map-popup      (map-click circumstances; Details panel shows them)
         #map-status (the date/type pill, lower left) is SHOWN on desktop too, as
         on the phone (user 2026-10-20). */
      #map-popup { display: none !important; }
    }


/* ── Sun-track diagram (Details panel) ─────────────────────────────────── */
#suntrack { margin: 2px 0 6px; }
.st-svg {
  width: 100%; height: auto; display: block;
  border: 1px solid var(--gold-dim);
  border-radius: 6px;
}
.st-track   { fill: none; stroke: rgba(255,255,255,0.55); stroke-width: 1.25; stroke-dasharray: 2 3; }
.st-horizon { stroke: rgba(255,255,255,0.35); stroke-width: 1; stroke-dasharray: 3 3; }
.st-hlbl    { fill: rgba(255,255,255,0.6); font-size: 9px; }
.st-ground  { fill: rgba(0,0,0,0.30); stroke: none; }
.st-olbl    { fill: rgba(255,255,255,0.9); font-size: 8px; paint-order: stroke;
              stroke: rgba(0,0,0,0.7); stroke-width: 2px; }
.st-contact { fill: #fff; stroke: rgba(0,0,0,0.5); stroke-width: 1; }
.st-clbl    { fill: #fff; font-size: 10px; paint-order: stroke; stroke: rgba(0,0,0,0.55); stroke-width: 2px; }
.st-sun     { fill: #ffe08a; filter: drop-shadow(0 0 5px rgba(255,210,120,0.9)); }
.st-moon    { fill: #0a0c10; stroke: rgba(255,255,255,0.25); stroke-width: 0.5; }
.st-slider  { width: 100%; margin: 5px 0 1px; accent-color: var(--gold); }
.st-readout { font-size: 11px; color: var(--gold2); text-align: center; font-variant-numeric: tabular-nums; white-space: nowrap; }

/* Floating map status overlay (eclipse label / "tap to set location") */
.map-status {
  /* Bottom-LEFT, not centred: centred, it sat right where the paths and the pin are,
     and fought the terrain for legibility. Cornered, it's always findable and never
     covers the thing you're looking at. */
  position: absolute; bottom: 0.85rem; left: 0.85rem;
  font-family: var(--mono); font-size: 0.78rem; color: var(--text);
  font-weight: 700; letter-spacing: 0.02em;
  pointer-events: none; background: var(--bg2);
  padding: 0.3rem 0.85rem; border-radius: 99px; opacity: 1;
  box-shadow: 0 1px 6px rgba(0,0,0,0.35);
  white-space: nowrap;          /* it was wrapping mid-date */
  max-width: calc(100% - 1.5rem);
  display: none;
}

/* ── Historic eclipses (js/historic.js) ──────────────────────────────────
   Row icon beside the date, and a desktop-only badge in the map's lower-left
   corner. Both are links that open a new tab. The temple MARK is gold (a glyph,
   like the instructions ornament — test_hygiene's GLYPHS); words never are, since
   gold text means "selected" in this app. */
.hist-link {
  display: inline-flex; vertical-align: -0.15em; margin-left: 0.4em;
  color: var(--gold); opacity: 0.85;
}
.hist-link:hover { opacity: 1; }
.hist-link svg, .hist-badge svg { width: 1em; height: 1em; flex: none; }
.hist-badge { display: none; }
@media (min-width: 900px) {
  .hist-badge:not([hidden]) {
    display: inline-flex; align-items: center; gap: 0.45em;
    position: absolute; left: 0.85rem; bottom: 0.85rem; z-index: 5;
    max-width: calc(100% - 1.7rem);
    font-family: var(--mono); font-size: 0.78rem; font-weight: 700;
    color: var(--text); text-decoration: none;
    background: var(--bg2); padding: 0.3rem 0.8rem; border-radius: 99px;
    box-shadow: 0 1px 6px rgba(0,0,0,0.35);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .hist-badge svg { color: var(--gold); }      /* the mark is gold; the words are not */
  .hist-badge:hover span:first-of-type { text-decoration: underline; text-underline-offset: 2px; }
  /* Lift it clear of the terrain-shadow time bar when that is showing. */
  .shadow-timeline:not([hidden]) ~ .hist-badge { bottom: calc(var(--hist-lift, 4.5rem)); }
  /* The date pill (#map-status) now shares this corner on desktop: the badge
     stacks above it, and both lift clear of the time bar. */
  .hist-badge:not([hidden]) { bottom: 2.85rem; }
  .shadow-timeline:not([hidden]) ~ .hist-badge { bottom: calc(var(--hist-lift, 4.5rem) + 2rem); }
  #tab-map:has(.shadow-timeline:not([hidden])) .map-status { bottom: var(--hist-lift, 4.5rem); }
}

/* Time-mode toggle arrows affordance */
.tm-switch { color: var(--text); cursor: pointer; text-decoration: underline dotted; text-underline-offset: 2px; }
.tm-switch:hover { color: var(--text-pale); }

.tol-note { margin-top: 0.5rem; }

.ct-row { cursor: pointer; }
.ct-row:hover td { background: rgba(200,169,110,0.12); }


/* ── Settings / About body text ────────────────────────────────────────────
   These panels were inheriting a larger base size than the rest of the app, so on
   mobile the prose read noticeably bigger than everything else. Pin them to the
   standard body size. */
#tab-settings p,
#tab-settings li,
#tab-settings .settings-group > div,
#tab-settings .top-section-header + p {
  font-size: 0.75rem;      /* matches the app's standard body size (was ~1rem → read as huge) */
  line-height: 1.55;
}


/* ── Asset loading crawlbar ────────────────────────────────────────────────
   A 1px white bar riding the banner's bottom hairline. Indeterminate: it crawls
   while any tracked fetch is in flight, so "the page is up but still loading" is
   visible rather than guessed at. */
.load-bar {
  position: relative;
  height: 1px;
  width: 100%;
  overflow: hidden;
  background: transparent;
  opacity: 0;
  transition: opacity 0.25s ease;
  pointer-events: none;
  z-index: 50;
}
.load-bar.active { opacity: 1; }
.load-bar-run {
  position: absolute;
  top: 0; left: 0;
  height: 100%;
  width: 22%;
  /* Solid segment with only slightly softened ends — a steady crawl. The old
     transparent→white→transparent gradient pulsed like a lightning zap. */
  background: rgba(255,255,255,0.85);
  border-radius: 1px;
  animation: load-crawl 2.4s linear infinite;   /* slower, calmer */
}
@keyframes load-crawl {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(555%); }
}

/* ── Hamburger throb: fresh local circumstances are waiting in Details ────────
   Brightens as well as swells. `fill` carries the colour change (a brightness
   multiply alone only shifts value, which reads as a wobble).

   The filter list must hold the SAME functions in the SAME order at every stop:
   CSS interpolates filter lists smoothly only when they match, and otherwise
   falls back to discrete swapping, which snaps at the midpoint. So the glow is
   always present and only its blur radius grows from 0, and brightness is always
   present and only its factor rises. The base drop-shadow from .tab-icon-svg is
   repeated here too, since a keyframe `filter` replaces the base one outright.
   Raise brightness() for a hotter flash. */
@keyframes tab-throb {
  0%, 100% {
    transform: scale(1);
    fill:      var(--gold);
    filter:    drop-shadow(0 1px 1px rgba(0,0,0,0.35))
               drop-shadow(0 0 0 var(--gold2))
               brightness(1);
  }
  50% {
    transform: scale(1.18);
    fill:      var(--gold2);
    filter:    drop-shadow(0 1px 1px rgba(0,0,0,0.35))
               drop-shadow(0 0 6px var(--gold2))
               brightness(1.6);
  }
}
@media (max-width: 899px) {
  .tab-btn.has-fresh .tab-icon-svg {
    animation: tab-throb 1.1s ease-in-out 1;    /* a single pulse is enough */
    transform-origin: center;
  }
}

/* ── Phone in LANDSCAPE: reclaim vertical space ────────────────────────────
   Browser chrome + our banner + tab bar left almost nothing for the map. In a short
   landscape viewport, drop the banner entirely (the tab bar already identifies the
   app) and tighten the tabs. */
@media (max-width: 950px) and (orientation: landscape) and (max-height: 500px) {
  :root { --header-h: 0rem; }
  .app-header { display: none; }

  .tab-btn {
    padding: 0.3rem 0.55rem;
    font-size: 0.66rem;
  }
  .tab-label { display: none; }        /* icons only — the labels are the fat here */
  .tab-icon-svg { width: 1em; height: 1em; }

  .map-status { bottom: 1.2rem; font-size: 0.7rem; padding: 0.2rem 0.6rem; }
}

/* ── Install prompt ────────────────────────────────────────────────────────
   Mobile BROWSERS only. Hidden on desktop (irrelevant) and in the installed
   standalone app (<html class="is-standalone">, where it would be absurd). */
.install-note { display: none; }
@media (max-width: 899px) {
  html:not(.is-standalone) .install-note {
    display: block;
    margin-top: 0.6rem;
    padding: 0.5rem 0.7rem;
    border-left: 2px solid var(--border2);
    background: rgba(200,169,110,0.08);
  }
}
