        /* ══════════════════════════════════════════════════════════════
           THE MODAL — the box, the sheet it slides up as, and the head it wears.
           .modal, .modal-content, .modal-sheet, .modal-header/-title/-subtitle/-close/-body,
           and the two keyframes they animate with.

           Renamed from 09-modal.css on 2026-09-22, which by then it had almost nothing
           to do with. The journal, the editor, the diary photographs, the activity detail page,
           the weather spinner and the trip settings all left it for their own parts; what was
           left was the chrome every modal in the app is built from, and that has NO SURFACE.
           Same reason 18-overlays.css and 06-pagestack.css are their own parts.

           THE NUMBER STAYS 9. It is a base — eight rooms scope their own rules onto .modal-content
           alone — so it has to load before them, and renaming must not move it.

           Zero outgoing-world tokens. The last one left with the FAB, above.
           ══════════════════════════════════════════════════════════════ */
        /* The diary photographs went to css/17q-editor.css on 2026-09-22 — or rather, they were
           already there and this was the losing half. .diary-photo-grid, -item, -item img,
           -remove and -remove:hover: every property each declared is re-declared by a
           #writeJournalModal-scoped rule, which always wins. Only three survived — the flex
           centring that puts the × in the middle of its corner — and they are beside that rule now.

           Safe because the class cannot appear anywhere else: 35-markup-journal.js:107 puts the
           grid inside #writeJournalModal and 07-diary.js:157 renders every item into that grid by
           id. Checked, because a class worn by a second surface would still be dressed by the rule
           below and nothing else. */

        /* The journal list's rules moved to css/17n-journal-list.css on 2026-09-11, when
           the room was migrated into the deck log. They are not duplicated: this file no
           longer styles `.jrnl-` anything that belongs to the list. */

        /* `.jrnl-link-pill` went with them, and it had already been dead — no markup in
           public/ has emitted it since the card's linked strip became `.jrnl-card-link`,
           which is itself gone now (a link inside a link). */

        /* The write modal's chrome stood here and was REMOVED 2026-09-21: .modal-title,
           .modal-header (and the ::before that printed CAPTURE THE DAY), .form-label, .form-input,
           .rich-text-toolbar, .form-group and .btn-primary, all scoped to #writeJournalModal.
           The room is built by put('writeJournalModal') in 35-markup-journal.js and holds none of
           them — the live DOM counts 0 for every one, and only .modal-content and
           .rich-text-editor survive (the .expanded rule above is kept for the latter).
           ⚠️ CAPTURE THE DAY has not been printed for as long as .modal-header has been gone; the
           eyebrow was lost in a rebuild, not in this deletion. Recorded in DEBTS rather than
           restored, because what the head should say is the owner's call.
           Seven outgoing-world tokens went with the block. */
        /* link picker */

        /* THE FAB WENT 2026-09-22, AND SO DID THE PAGE IT LIVES ON.

           `.fab`, `.fab:hover`, `.fab svg` and their @media (min-width: 431px) dressed
           `<button class="fab" id="fabBtn">`, which 33-markup-trip.js:18 puts inside
           #detailPage. `.page` is `display: none` (01-tokens.css:327) until it is `.active`,
           and NOTHING EVER ACTIVATES #detailPage — 02-state.js:1939 says so in as many words:
           "no code path anywhere calls showPage('detailPage'), so the whole chain was
           unreachable". So this was a filled brown square, white glyph, hover scale(1.1), on a
           control no one can reach. It carried this file’s LAST outgoing-world token.

           ⚠️ 07-diary.js STILL MAINTAINS IT. updateFabButton() sets fabBtn to display:flex in
           four branches and positionFabDesktop() walks it on every scroll, all for an element
           inside a page that never shows. Left alone here because this round moves stylesheets,
           not behaviour, and because 03-map-plan.js:884 already notes that the FAB the Media and
           Diary sheets actually use is a DIFFERENT element. DEBTS.

           `.dropdown { position: relative }` went with it: nothing in the app wears that class.
           Every hit a grep returns is `currency-dropdown` or `dropdown-arrow`, which merely
           contain the word. */

        /* Modal */

        .modal {
            display: none;
            position: fixed;
            inset: 0;
            background: rgba(22, 25, 26, 0.42);   /* the oilskin, not the outgoing #242424 (2026-09-17) */
            z-index: 1000;
            padding: var(--sp-lg);
            overflow-y: auto;
            overscroll-behavior: contain;
        }

        .modal.active {
            display: flex;
            /* NOT align-items: center.
               A flex item taller than its container gets centred by overflowing equally at
               both ends — and the overflow above the container's top edge cannot be
               scrolled to, in any browser. That is why a long activity card opened with
               its title already cut off: the card was centred, and the top third was
               above the scrollable area entirely.
               `margin: auto` on the content centres it when it fits and pins it to the top
               when it doesn't, which is what centring should have meant all along. */
            align-items: flex-start;
            justify-content: center;
        }

        .modal-content {
            background: var(--log-stock);
            border-radius: 0;
            max-width: 600px;
            width: 100%;
            /* Centred when it fits, top-anchored when it doesn't. See .modal.active. */
            margin: auto;
            /* dvh, not vh. On a phone `vh` is resolved against the viewport with the
               address bar hidden, so 90vh is taller than 90% of what is actually on
               screen while the bar is showing — the card overflows the space it was
               supposed to fit inside. */
            max-height: 90dvh;
            overflow-y: auto;
            overscroll-behavior: contain;
            -webkit-overflow-scrolling: touch;
            animation: modalSlide 0.3s ease-out;
        }

        /* ── carried from css/10-legacy-home-sheet.css on 2026-09-21, as that part was emptied.
           It has always overridden the rule directly above, from the very next stylesheet along,
           so beside it is where it belonged. On a phone the card is the screen: no margin, no
           corner, full height. (border-radius is already 0 above; kept so the block reads as one
           intention rather than two-thirds of one.) */
        @media (max-width: 768px) {
            .modal-content {
                margin: 0;
                border-radius: 0;
                max-height: 100vh;
            }
        }

        .modal-sheet.active {
            align-items: flex-end;
            padding: 0;
        }

        .modal-sheet .modal-content {
            max-width: 100%;
            width: 100%;
            /* Bottom sheets anchor to the bottom, so margin:auto would fight that. */
            margin: 0;
            max-height: 85dvh;
            border-radius: 0;
            animation: sheetSlideUp 0.35s ease-out;
            display: flex;
            flex-direction: column;
        }

        .modal-sheet .modal-header {
            width: 90%;
            margin: 0 auto;
            padding: 2rem 0 1rem;
            border-bottom: 1px solid var(--log-rule);
        }

        .modal-sheet .modal-body {
            width: 90%;
            margin: 0 auto;
            padding: 0 0 2rem;
            flex: 1;
            overflow-y: auto;
            -webkit-overflow-scrolling: touch;
        }

        @keyframes sheetSlideUp {
            from {
                opacity: 0;
                transform: translateY(100%);
            }
            to {
                opacity: 1;
                transform: translateY(0);
            }
        }

        @keyframes modalSlide {
            from {
                opacity: 0;
                transform: translateY(30px);
            }

            to {
                opacity: 1;
                transform: translateY(0);
            }
        }

        .modal-header {
            padding: 2rem 2rem 1rem;
            display: flex;
            justify-content: space-between;
            align-items: flex-start;
        }

        .modal-title {
            /* 阶段一：删全大写。planTripModal / expenseModal / writeJournalModal 三处早已 text-transform:none，这里只是把它变成默认。 */
            font-family: var(--log-write);
            font-size: 19px;
            font-weight: var(--fw-regular);
            /* 阶段一：删 letter-spacing —— 字距是 caps token 的配套，正常大小写不需要 */
        }

        .modal-subtitle {
            font-size: var(--fs-small);
            letter-spacing: 2px;
            text-transform: uppercase;
            color: var(--log-ink-soft);
        }

        .modal-close {
            background: none;
            border: none;
            font-size: var(--fs-display);
            color: var(--log-ink-soft);
            cursor: pointer;
            line-height: 1;
            padding: 0;
            width: 32px;
            height: 32px;
        }

        .modal-body {
            padding: 1.5rem 2rem 2rem;
        }

        /* @keyframes wxspin went to css/17zb-weather.css on 2026-09-22, with the .wx-spinner
           that is the only thing naming it. The mover does not take at-rules, so it had to be
           carried by hand — the same near-miss as @keyframes jwFadeIn on 2026-09-21, and the
           same reason it matters: a keyframe is global, so the animation keeps working from any
           file until the file is DELETED, and then it names nothing. NO PHOTOGRAPH CAN CATCH
           THAT. A spinner that does not spin is a still picture of a spinner. */

        /* The checklist's rules were here: blocks on two tinted grounds, a tick drawn in a filled box, struck-
           through words, an accent × and + Add, and a dotted line to write on. REMOVED on 2026-09-14, not
           overridden: the page is in the deck log (css/17za-checklist.css). */

        /* Dropdown Menu */
