
        /* ══════════════════════════════════════════════════════════════
           MOVED OUT OF css/09-modal.css · 2026-09-21  (prepended)
           1 rules. DESIGN.md: a surface's rules go in that surface's part, and the
           legacy parts only shrink.
           ⚠️ 09-modal.css loaded BEFORE this file. Placing them
           at the head keeps their order against THIS PART’S OWN rules — and nothing else.
           Against the parts BETWEEN #25 and #8 the order is reversed by this move, and
           whether that matters was a separate question somebody had to answer. See DESIGN.md.
           ══════════════════════════════════════════════════════════════ */
        /* The page UNDER a card stays put. The stack nudges a covered page left so a
           sliding page reads as depth; under a card that nudge reads as the screen
           lurching. :has(+ …) — the covered page is the card's previous sibling. */
        .pgstack-root .pgstack-page.covered:has(+ .pgstack-page.act-page) { transform: none; }


        /* ══════════════════════════════════════════════════════════════
           MOVED OUT OF css/09-modal.css · 2026-09-21  (prepended)
           11 rules. DESIGN.md: a surface's rules go in that surface's part, and the
           legacy parts only shrink.
           ⚠️ 09-modal.css loaded BEFORE this file. Placing them
           at the head keeps their order against THIS PART’S OWN rules — and nothing else.
           Against the parts BETWEEN #25 and #8 the order is reversed by this move, and
           whether that matters was a separate question somebody had to answer. See DESIGN.md.
           ══════════════════════════════════════════════════════════════ */
        /* Activity detail — a page on the stack (2026-09-02), was #activityDetailModal;
           presented as a CARD on a scrim (2026-09-03), which is what the boarding pass
           was designed as. See openActivityDetailPage in 05-timeline.js.

           The page is the scrim: the modal's own colour, so the surround reads as
           "outside" the way it always did (a tap there closes). Its header is hidden;
           it fades rather than slides — the .pgstack-root prefix outranks the stack's
           own .entered / .covered transforms, which are written for sliding pages. */
        .pgstack-root .pgstack-page.act-page {
            transform: none;
            opacity: 0;
            transition: opacity 0.25s ease;
            background: rgba(22, 25, 26, 0.42);   /* the oilskin, not the outgoing #242424 (2026-09-17) */
        }

        .pgstack-root .pgstack-page.act-page.entered { opacity: 1; }

        .pgstack-root .pgstack-page.act-page.covered { transform: none; }

        .pgstack-page.act-page .pgstack-header { display: none; }

        /* Column, so the card can centre itself with margin:auto when it fits and
           scroll when it doesn't (justify-content:center would clip a tall card's top). */
        .pgstack-page.act-page .pgstack-body {
            display: flex;
            flex-direction: column;
            padding: 24px 0;
            overscroll-behavior: contain;
        }

        /* The plain view's panel: the old .modal-sheet surface, 90% wide, capped so the
           scrim stays visible above and below, scrolling inside. Rises a touch as the
           scrim fades in. */
        .act-card {
            margin: auto;
            width: 90%;
            max-height: 100%;
            overflow-y: auto;
            -webkit-overflow-scrolling: touch;
            /* The card carried the SAME ground as the sheet under it and was told apart by
               its rule alone — so both go to the stock and the rule does the work it always
               did. Giving the card a band here would be a new distinction, not a conversion. */
            background: var(--log-stock);
            border: 1px solid var(--log-rule);
            transform: translateY(16px);
            transition: transform 0.25s ease;
        }

        .pgstack-page.entered .act-card { transform: none; }

        /* While a finger is dragging it, the card follows the finger — no easing. */
        .act-card.is-dragging { transition: none; }

        .act-card #activityDetailContent { padding: 0 20px; }

        /* Ticket views (flights / transit / stays / sightseeing / generic card): the
           ticket IS the card — it brings its own header and pencil — so the panel goes
           transparent and full width and its head and action bar are dropped. The
           ticket's own max-width:340px / margin:auto centres it, exactly as it sat on
           the transparent sheet. */
        .pgstack-page.act-page.bp-fullbleed .act-card {
            width: 100%;
            max-height: none;
            overflow: visible;
            background: transparent;
            border: none;
        }

        .pgstack-page.act-page.bp-fullbleed #activityDetailContent { padding: 0; }


        /* ══════════════════════════════════════════════════════════════
           MOVED OUT OF css/06-legacy-map-stops.css · 2026-09-21  (prepended)
           1 rules. DESIGN.md: a surface's rules go in that surface's part, and the
           legacy parts only shrink.
           ⚠️ 06-legacy-map-stops.css loaded BEFORE this file. Placing them
           at the head keeps their order against THIS PART’S OWN rules — and nothing else.
           Against the parts BETWEEN #26 and #6 the order is reversed by this move, and
           whether that matters was a separate question somebody had to answer. See DESIGN.md.
           ══════════════════════════════════════════════════════════════ */


        /* ⚠️ ONLY `flex: 1` DRAWS HERE. css/24-tail.css, which loads last, sets this element’s
           padding-bottom at :150 and its other three sides at :168 — so every side of the
           shorthand below is overridden and has been since the ledger rows were widened to the
           full page. Kept rather than cut: it is the value 24-tail is arguing WITH, and the two
           are worth reading together. (Noted 2026-09-22, coming out of 06-legacy-map-stops.css.) */
        .trip-plan-body {
            padding: 1rem 1.5rem 1.5rem;
            flex: 1;
        }

        /* ==========================================================================
           #U  The journey — its stops, its days, and the chart of it
           --------------------------------------------------------------------------
           Migrated into the deck log on 2026-09-09. Drawn by public/04-stops.js
           (the stops) and public/03-map-plan.js (the map).

           WHAT IT REPLACES. This was the last big room of the outgoing world, and it wore
           the whole costume: a 320px map hero with two unlabelled icon buttons floating on
           it, a sheet pulled up over the map's foot by a negative margin, a serif trip
           name, brown-on-cream, a four-tab bar along the bottom, and a filled square FAB
           with a plus in it. The six trip files between them used the deck log's tokens
           ZERO times.

           The chart had the same shape and was answered on 2026-09-07: the plate is the
           page, closed by the rules around it rather than floated on. Same answer here.

           WHAT THE OWNER DECIDED, 2026-09-09:
           · Both maps — a short band that stays while you read, AND a whole-screen view.
             One canvas at two heights, never two instances: the chart's boot race taught
             this app what a second map costs.
           · Stops and Days stay two views, switched by words. The bottom tab bar goes.
           · The floating + stays this round, and is dealt with next. It is the one thing
             on this page that still belongs to the outgoing world, and it is named here so
             the next reader knows it is a decision and not an oversight.
           ========================================================================== */
        #tripMapPage {
            background: var(--log-stock);
        }

        #tripMapPage.active {
            display: flex;
            flex-direction: column;
            height: 100vh;
            height: 100dvh;
            min-height: 0;
            overflow: clip;
        }

        /* ---- the head: Rooms-off-Home ------------------------------------------- */
        #tripMapPage .tp-bar {
            flex: none;
            display: grid;
            grid-template-columns: 1fr auto 1fr;
            align-items: center;
            gap: var(--sp-sm);
            padding: calc(var(--sp-2xs) + env(safe-area-inset-top, 0px)) var(--sp-lg) var(--sp-2xs);
            background: var(--log-stock);
            border-bottom: 1px solid var(--log-rule-firm);
        }

        /* The journey's own name, in the instrument's voice rather than the traveller's.
           It was a serif line in sentence case, which read as the title of a book about the
           trip; this is the heading of the sheet the trip is kept on. */
        #tripMapPage .tp-title {
            margin: 0;
            font-family: var(--log-print);
            font-size: var(--fs-small);
            font-weight: 600;
            letter-spacing: 0.16em;
            text-transform: uppercase;
            line-height: 1.2;
            color: var(--log-ink);
            text-align: center;
            /* A journey can be called anything. One line, and the ends of a long name are
               cut by the sheet rather than pushing Back and Details off their edges. */
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
            max-width: 46vw;
        }

        #tripMapPage .tp-back,
        #tripMapPage .tp-act {
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.16em;
            text-transform: uppercase;
            text-decoration: underline;
            text-underline-offset: 0.25em;
            text-decoration-thickness: 1px;
            background: none;
            border: none;
            padding: 0;
            cursor: pointer;
            color: var(--log-ink);
            display: inline-flex;
            align-items: center;
            min-height: 44px;
        }
        #tripMapPage .tp-back { justify-self: start; }
        #tripMapPage .tp-act { justify-self: end; }

        /* ---- the tally: what the instrument below holds -------------------------- */
        /* The Two Jobs of Prose Rule. It replaces a coloured bar with a red hatched tail
           and the line "1 of 13 nights unplanned" printed under it in alarm red — a chart
           of one number, and a colour doing the work of a word. */
        #tripMapPage .tp-tally-block {
            flex: none;
            padding: var(--sp-sm) var(--sp-lg);
            border-bottom: 1px solid var(--log-rule-firm);
        }

        /* ONE CELL PER NIGHT, in order (owner, 2026-09-09: the progress of the plan has to
           be legible at a glance, which a sentence alone is not).

           Not the outgoing world's bar returning. That one was a flex row of brown segments
           measuring the stops' own allocation, with a 45-degree hatch in #9B4D4D for the
           remainder — a warning colour spent on somebody who had not yet decided where to
           sleep on the Tuesday. This is read from the bookings, and it is one ink at three
           pressures: a bed is the ink, a night in transit is the soft ink, a night with
           neither is bare stock. A gap is a gap, not an alarm.

           The hairlines between cells are the stock showing through, so the scale reads as
           ruled paper rather than as a painted bar. */
        /* A POINTER AT TONIGHT, IN THE MAGENTA, under the cell it points at (owner,
           2026-09-09).

           The colour was reserved for acts that change the record and this is not an act,
           which is how it came to be an ink block above the scale and to be missed. The
           reservation was too narrow: this app already spends magenta on a mark that is not
           an act — the SPECIMEN stamped across Home's empty-state cachet — and that is what
           the colour does on a chart, marking what was laid over a printed sheet after it
           was printed. Tonight's position on a standing plan is exactly that.

           A triangle, because it has to say WHICH of seventeen cells without recolouring one
           and taking its own reading away. Drawn with borders rather than a glyph: it is a
           mark on an instrument, the same kind of thing as the arrowheads on the chart's
           tracks, and there is no character in this world's alphabet for it. */
        #tripMapPage .tp-scale-marks {
            display: grid;
            gap: 1px;
            height: 6px;
            margin: 2px 0 var(--sp-xs);
        }
        #tripMapPage .tp-scale-marks[hidden] { display: none; }
        #tripMapPage .tp-mark { position: relative; }
        #tripMapPage .tp-mark.is-now::before {
            content: '';
            position: absolute;
            left: 50%;
            top: 0;
            transform: translateX(-50%);
            width: 0;
            height: 0;
            border-left: 4px solid transparent;
            border-right: 4px solid transparent;
            border-bottom: 6px solid var(--log-magenta);
        }

        #tripMapPage .tp-scale {
            display: grid;
            gap: 1px;
            height: 10px;
            /* No gap under the scale: the pointer belongs against it. The 8px that used to
               sit here swallowed the marks row's own 3px margin (adjacent margins collapse
               to the larger) and left the triangle floating eight pixels below the night it
               points at. */
            margin-bottom: 0;
            background: var(--log-rule);
            border: 1px solid var(--log-rule-firm);
        }
        #tripMapPage .tp-scale[hidden] { display: none; }
        #tripMapPage .tp-night { background: var(--log-stock); }
        #tripMapPage .tp-night.is-bed { background: var(--log-ink); }
        #tripMapPage .tp-night.is-travel { background: var(--log-ink-soft); }

        #tripMapPage .tp-tally {
            margin: 0;
            padding: 0;
            font-family: var(--log-write);
            font-size: var(--fs-small);
            line-height: 1.45;
            color: var(--log-ink-soft);
        }
        /* The nights still unaccounted for are the one thing on this line worth raising,
           and they are raised in the ink, at full pressure — not in a warning colour. An
           unplanned night is not an error. */
        #tripMapPage .tp-tally b {
            font-weight: 400;
            color: var(--log-ink);
        }

        /* ---- the plate: the chart of this journey -------------------------------- */
        /* Edge to edge, closed by rules, nothing floated on it. The two icon buttons that
           used to sit on the map are gone: Back and Details are words in the head, where
           they can be read. */
        /* THE PLATE IS THE MAP VIEW, and nothing else (owner, 2026-09-09).

           It was a 248px band above the sheet. The band was the only reserve on this page
           big enough to matter — head 53 + tally 95 + plate 248 + words 45 left the sheet
           371 of 812, five rows of nine — and the map already had a view of its own, so the
           band was a second answer to a question already answered. Gone from Stops, the
           sheet takes 619.

           The canvas stays in the DOM so one map instance serves both; it is built the
           first time the view is opened (ensureTripMap) rather than while it is hidden,
           because a Google map built in a container of no size comes up grey and a later
           resize does not reliably rescue it. */
        #tripMapPage .tp-plate {
            display: none;
            position: relative;
            background: var(--log-stock-band);
            overflow: hidden;
        }
        #tripMapPage.is-map .tp-plate {
            display: block;
            flex: 1;
            min-height: 0;
            border-bottom: 1px solid var(--log-rule-firm);
        }
        #tripMapPage .tp-canvas {
            position: absolute;
            inset: 0;
        }

        /* ---- the map kept for no signal (2026-09-18, 40-offline-map.js) ---------------- */
        /* The same plate, the same atlas paper and the same marks as Google's map: a stop is its number in the ink, and a
           place visited twice is one pill. The note says, in the print of every label, that this is the kept map and when
           it was kept — so a map drawn from a copy is not read as a live one. */
        #tripMapPage .offmap { position: absolute; inset: 0; background: #E9E4D6; }
        #tripMapPage .offmap-stop {
            min-width: 26px;
            height: 26px;
            box-sizing: border-box;
            padding: 0 7px;
            border: 2px solid #E9E4D6;
            border-radius: 13px;
            background: var(--log-ink);
            color: #E9E4D6;
            font-family: var(--log-print);
            font-size: 12px;
            font-weight: 600;
            font-variant-numeric: lining-nums tabular-nums;
            line-height: 22px;
            text-align: center;
            white-space: nowrap;
        }
        #tripMapPage .offmap-note {
            position: absolute;
            top: var(--sp-xs);
            left: var(--sp-xs);
            margin: 0;
            padding: 4px var(--sp-xs);
            background: var(--log-stock);
            border: 1px solid var(--log-rule-firm);
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.14em;
            text-transform: uppercase;
            color: var(--log-ink);
            pointer-events: none;
        }
        #tripMapPage .maplibregl-ctrl-attrib { font-size: 10px; background: rgba(233, 228, 214, 0.85); }

        /* One canvas, one instance. A second is what re-armed the chart's white plate for
           a day: two painters, one visible, and the loser calling setMap(null) on the
           winner's markers. */
        #tripMapPage.is-map .tp-sheet { display: none; }
        #tripMapPage.is-map .tp-tally-block { display: none; }

        /* ---- the views: three readings of one journey, and a door ---------------- */
        /* At the foot of the page, and still there when the map opens (owner, 2026-09-09).
           A rule above it rather than below: it now closes the page instead of opening the
           sheet.

           TWO ROOMS SHARE IT. Boswell, entered from a journey, carries the same four words
           at its own foot — it is the same furniture doing the same job, and it was a copy
           of the outgoing world's tab bar until 2026-09-09. Listing both here rather than
           copying the rules is the point: the last copy said ITINERARY six days after that
           view was renamed. */
        /* THREE ROOMS SHARE IT since 2026-09-09: the second home joined, because the day
           you are in is a reading of the journey like the other three and the owner asked
           for the same bar under it. */
        #tripMapPage .tp-views,
        #boswellPage .tp-views,
        #mainPage .tp-views {
            flex: none;
            display: flex;
            align-items: center;
            gap: 0 var(--sp-lg);
            padding: 0 var(--sp-lg) env(safe-area-inset-bottom, 0px);
            border-top: 1px solid var(--log-rule-firm);
            background: var(--log-stock);
        }

        #tripMapPage .tp-view,
        #boswellPage .tp-view,
        #mainPage .tp-view {
            min-height: 44px;
            padding: 0;
            background: none;
            border: none;
            cursor: pointer;
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.16em;
            text-transform: uppercase;
            color: var(--log-ink-soft);
        }
        #tripMapPage .tp-view.is-on,
        #boswellPage .tp-view.is-on,
        #mainPage .tp-view.is-on {
            color: var(--log-ink);
            text-decoration: underline;
            text-underline-offset: 0.25em;
            text-decoration-thickness: 1px;
        }
        /* Boswell is not a fourth reading of this journey — it is another room, and the
           tab bar said otherwise by putting it in the same row as the three that are.
           Pushed to the far end, the way the chart's Save sits apart from its papers:
           the position says "this one leaves". */
        #tripMapPage .tp-view.is-door,
        #boswellPage .tp-view.is-door,
        #mainPage .tp-view.is-door {
            margin-left: auto;
            text-decoration: underline;
            text-underline-offset: 0.25em;
            text-decoration-thickness: 1px;
            color: var(--log-ink);
        }

        /* .has-today lived here for an afternoon, closing the gap to 16px so five words
           would fit a 375-wide phone. TODAY moved to the head of the rooms it led to
           (owner, 2026-09-09), so the bar is four words again and the gap is the world's. */

        /* ---- the sheet ---------------------------------------------------------- */
        #tripMapPage .tp-sheet {
            flex: 1;
            min-height: 0;
            overflow-y: auto;
            -webkit-overflow-scrolling: touch;
            background: var(--log-stock);
            padding-bottom: var(--sp-lg);
        }

        /* The strip the floating + stood on was here, and went with the button on
           2026-09-09. Both of its acts are lines in the ledger now — ADD A STOP at the foot
           of the Stops sheet, ADD TO THIS DAY at the foot of each day — so the sheet keeps
           those 60px on every view, and nothing on this page floats over the record. */
        /* Every row on the sheet shares one grid: a margin column the width of a stop
           number, then the entry. The Margin-Column Rule — the number is a reference, not
           a decoration, so it sits in the margin where a reference goes, and the entries
           all start on one line down the page. */
        /* THE COLUMN RULE — the thread the whole journey hangs on.

           Reported 2026-09-09: "一眼看不出 stop 与 stop 的连接". Measured, the complaint was
           exact. A place name was 13.6px at weight 400 and a leg's mode was 11px at weight
           600 in letter-spaced caps, in the SAME ink — so FLIGHT and Yogyakarta arrived at
           the eye with the same force, and a reader scanning the sheet met an even stripe of
           rows with no way to tell a station from the line between two.

           `--log-rule-firm` is documented in the token block as what draws COLUMN RULES,
           which carry meaning, as against `--log-rule`, the hairline between rows that never
           does. This is the case it was defined for. One rule runs the length of the ledger,
           unbroken through the legs; the stops' numbers sit to its left as stations on it.
           The connection between two stops becomes a thing you can see rather than infer,
           and it cost one line.

           It stops at the two terminus bands. A ledger's opening and closing lines are not
           entries, and a thread running through them would say the journey continues past
           its own ends. */
        #tripMapPage .tp-row,
        /* ══════════════════════════════════════════════════════════════════════════════
           THE PLAN, AS ONE COMPOSITION · 2026-09-12
           ══════════════════════════════════════════════════════════════════════════════

           The owner, of the version before this: 乱七八糟的感觉呢 · 太不好看了. They were
           right, and the cause was not any single rule: the page was a ledger with cards
           floating on it, so two systems of edges ran down it at once and nothing shared a
           left edge with anything else. What follows is one grid, used by every row on the
           sheet.

               56px   |   1fr           |   auto
               plate  |   place, dates  |   nights

           A stop fills all three. A leg leaves the first empty so the thread runs through
           the plate's own centre and its words begin where the place names do. A gap fills
           the last two. Nothing on the page starts at a fourth left edge.

           Three groups per card, tight inside and generously apart — the photograph, the
           name with its dates two pixels under it, and the count. No inner boxes, no second
           border, no tint: the card's own rule is the only edge it draws. */

        #tripMapPage .tp-row,
        #tripMapPage .tp-leg,
        #tripMapPage .tp-gap,
        #tripMapPage .tp-add {
            display: grid;
            /* ⚠️ 44, NOT 56 (owner, 2026-09-17: 整个页面看着太松散了). The narrow phone's sizes are every phone's
               now: the photograph, the rows and the thread that runs through its centre. */
            grid-template-columns: 44px minmax(0, 1fr) auto;
            align-items: center;
            gap: 0 var(--sp-md);
            box-sizing: border-box;
        }
        /* ⚠️ A LEG AND A GAP ARE ONE LINE, so each of their parts gets its own column
           rather than being stacked by auto-placement. grid-auto-flow: column was tried and
           does nothing here: a child with an explicit grid-column is placed by that, and two
           of them naming the same column stack whatever the flow says. Placement beats flow;
           if two things must sit side by side, they need two columns. */
        #tripMapPage .tp-leg { grid-template-columns: 44px auto minmax(0, 1fr) auto; }
        #tripMapPage .tp-leg > .tp-margin { grid-column: 1; }

        /* ---- a stop ------------------------------------------------------------- */
        #tripMapPage .tp-row {
            min-height: 60px;
            margin: var(--sp-2xs) var(--sp-lg);
            padding: var(--sp-xs);
            border: 1px solid var(--log-rule-firm);
            background: var(--log-stock);
            width: auto;
            text-align: left;
            font: inherit;
        }

        /* The photograph, and the stop's number knocked out of its corner. It is the one
           coloured thing on the sheet, so it is given a real size rather than a thumbnail's
           — and where there is no photograph there WAS no plate, only the figure, because a
           grey square pretending to be a picture is worse than an honest gap. */
        /* ⚠️ .tp-pic AND NOT .tp-plate, BECAUSE THIS FILE ALREADY OWNS THAT NAME — the
           map's own plate, which is display:none on this view. The photograph cell was
           written as .tp-plate first, inherited that, came out 0px wide, and grid's
           auto-placement slid the name into column one and the stepper into column two.
           The page looked scrambled and nothing in the rules said why.

           And renaming it by search-and-replace then hit the map's rule as well, which put
           display:none back on the photograph from the other direction. A class name is an
           interface, this file has two hundred of them, and a blanket rename is not a
           rename. */
        /* ⚠️ AN EMPTY PLATE AGAIN (owner, 2026-09-17: stop 没有图片的时候 显示一个空图占位).
           Without it the row's first column was blank and the number floated in space, so the
           card read as misaligned rather than as unillustrated. The plate is the band's tint
           inside the card's own rule, no icon: a frame waiting for its photograph. */
        #tripMapPage .tp-pic {
            position: relative;
            box-sizing: border-box;
            width: 44px;
            height: 44px;
            border: 1px solid var(--log-rule-firm);
            background-color: var(--log-stock-band);
            background-size: cover;
            background-position: center;
        }

        #tripMapPage .tp-margin {
            position: absolute;
            left: 0;
            bottom: 0;
            min-width: 18px;
            padding: 1px 4px;
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.1em;
            font-variant-numeric: lining-nums tabular-nums;
            text-align: center;
            color: var(--log-ink-soft);
        }
        #tripMapPage .tp-pic.has-image .tp-margin {
            background: rgba(22, 25, 26, 0.70);
            color: var(--log-on-oilskin);
        }

        /* Name and dates are ONE group: two pixels apart, and the pair centred against the
           photograph. The date line is printed in caps — the instrument's voice, because a
           span of days is a reference and not prose — which also sets it two clear steps
           below the name in size, weight and voice at once. */
        #tripMapPage .tp-open {
            display: grid;
            align-content: center;
            gap: 2px;
            min-height: 44px;
            min-width: 0;
            padding: 0;
            margin: 0;
            border: none;
            background: none;
            text-align: left;
            font: inherit;
            cursor: pointer;
        }
        #tripMapPage .tp-place {
            font-family: var(--log-write);
            font-size: var(--fs-base);
            line-height: 1.2;
            color: var(--log-ink);
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }
        #tripMapPage .tp-when {
            margin: 0;
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.12em;
            text-transform: uppercase;
            font-variant-numeric: lining-nums tabular-nums;
            color: var(--log-ink-soft);
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }
        #tripMapPage .tp-date { white-space: nowrap; }
        /* The place and, on the stop you are at, the HERE stamp on its line: the name gives way (ellipsis), the stamp never. */
        #tripMapPage .tp-name-line { display: flex; align-items: center; gap: var(--sp-xs); min-width: 0; }
        #tripMapPage .tp-name-line .tp-place { min-width: 0; }
        /* ⚠️ A STAMP THAT LOOKS STAMPED (owner, 2026-09-19: 可以像一个真的印章吗 像paid那种). Pressed onto the card, not
           printed in its line: a turn off the ruling, a double rule (the rubber's rim and its inner edge), the print face at
           its heaviest, and the magenta — the colour this world keeps for what is laid over the record. Flat: no shadow,
           no lift (Plate-Not-Float); a stamp is ink on the page. */
        #tripMapPage .tp-here {
            flex: none;
            margin-left: var(--sp-2xs);
            padding: 1px 6px 0;
            border: 3px double var(--log-magenta);
            color: var(--log-magenta);
            font-weight: 700;
            letter-spacing: 0.18em;
            line-height: 1.35;
            transform: rotate(-8deg);
            opacity: 0.92;
        }

        /* The count, as one block: the figure between its two marks, its unit printed under
           them. Seventy-two pixels instead of a hundred and forty spread across the row. */
        #tripMapPage .tp-nights {
            display: grid;
            grid-template-columns: repeat(3, auto);
            justify-items: center;
            align-items: center;
            column-gap: var(--sp-2xs);
            padding: 0;
        }
        /* ⚠️ ONE STEP DOWN (owner: nights的数字太大了 减小). At --fs-lg the figure was the
           largest thing on the sheet after the journey's own name — a count of nights
           shouting over the place it belongs to. It sits with the place name now, which is
           the level it belongs at: the same step, one weight above. */
        #tripMapPage .tp-step {
            grid-row: 1;
            min-width: 26px;
            min-height: 44px;
            margin: -8px 0;   /* the 44px target, laid out at 28 so the card is not held open to it */
            padding: 0;
            border: none;
            border-radius: 0;
            background: none;
            cursor: pointer;
            font-family: var(--log-print);
            font-size: var(--fs-base);
            font-weight: 400;
            line-height: 1;
            color: var(--log-ink);
        }
        #tripMapPage .tp-step:active { color: var(--log-magenta); }
        #tripMapPage .tp-count {
            grid-row: 1;
            grid-column: 2;
            min-width: 20px;
            font-family: var(--log-print);
            font-size: var(--fs-base);
            font-weight: 600;
            font-variant-numeric: lining-nums tabular-nums;
            line-height: 1;
            color: var(--log-ink);
        }
        #tripMapPage .tp-unit {
            grid-row: 2;
            grid-column: 1 / -1;
            margin-top: 2px;
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.14em;
            text-transform: uppercase;
            color: var(--log-ink-soft);
        }

        /* ---- what runs between them --------------------------------------------- */
        /* A leg is not an entry, so it draws no edge at all: the thread through the plate
           column and two lines of type. It is the quiet between two cards, which is the only
           thing that makes the cards read as cards. */
        #tripMapPage .tp-leg,
        #tripMapPage .tp-add {
            position: relative;
            min-height: 44px;
            margin: 0 var(--sp-lg);
            padding: var(--sp-2xs) var(--sp-xs);
            border: none;
            background: none;
            width: auto;
            text-align: left;
        }
        #tripMapPage .tp-leg::before,
        #tripMapPage .tp-add::before {
            content: '';
            position: absolute;
            left: calc(var(--sp-xs) + 22px);
            top: 0;
            bottom: 0;
            width: 1px;
            background: var(--log-rule-firm);
        }
        #tripMapPage .tp-leg > .tp-margin,
        #tripMapPage .tp-add > .tp-margin { position: static; padding: 0; }

        /* ⚠️ THE LEG SITS ON THE THREAD (owner marked the place in red). Its words began
           where the place names do, which lined it up with the cards and left the thread
           running past an empty column — a connector standing where an entry stands. It
           begins just clear of the thread now, so the eye reads card, thread, card, and the
           legs are plainly the things BETWEEN.

           And it is shorter: no vertical padding, so the row is its 44px target and nothing
           more (owner: 整个flight 行高稍微矮些 让页面更紧凑). */
        #tripMapPage .tp-leg {
            grid-template-columns: minmax(0, 1fr) auto;
            padding: 0 var(--sp-xs) 0 36px;
            min-height: 44px;
        }

        /* Flight │ Sydney - Bali · 4621km — one phrase, one rule between its two halves,
           and a dashed underline when there is a document behind it. */
        #tripMapPage .tp-leg-line {
            grid-column: 1;
            display: grid;
            grid-template-columns: auto auto minmax(0, 1fr);
            align-items: center;
            gap: 0 var(--sp-sm);
            min-width: 0;
            min-height: 44px;
            padding: 0;
            margin: 0;
            border: none;
            background: none;
            text-align: left;
            font: inherit;
        }
        #tripMapPage .tp-leg-line.is-doc { cursor: pointer; }
        #tripMapPage .tp-leg-line.is-doc .tp-leg-said {
            border-bottom: 1px dashed var(--log-rule-firm);
            padding-bottom: 1px;
        }
        #tripMapPage .tp-leg-line.is-doc:active .tp-leg-said {
            border-bottom-color: var(--log-ink);
        }

        /* ---- a leg with a mode: one press, its pieces at 28px a line ---------------- */
        /* See renderLegConnector: the acts are in the leg's sheet, so the plan prints what the leg IS. The
           button is the thread's own column plus the lines, 44px at the least, and the distance takes the
           right-hand column CHANGE used to hold. */
        #tripMapPage .tp-legs {
            position: relative;
            display: block;
            width: calc(100% - 2 * var(--sp-lg));   /* a button does not stretch to its line on its own */
            min-height: 44px;
            box-sizing: border-box;
            margin: 0 var(--sp-lg);
            padding: var(--sp-2xs) var(--sp-xs) var(--sp-2xs) 0;
            border: none;
            background: none;
            text-align: left;
            font: inherit;
            cursor: pointer;
        }
        #tripMapPage .tp-legs::before {
            content: '';
            position: absolute;
            left: calc(var(--sp-xs) + 22px);
            top: 0;
            bottom: 0;
            width: 1px;
            background: var(--log-rule-firm);
        }
        #tripMapPage .tp-leg-row {
            display: grid;
            grid-template-columns: auto auto minmax(0, 1fr) auto;
            align-items: center;
            gap: 0 var(--sp-sm);
            min-height: 28px;
            padding-left: 36px;
        }
        #tripMapPage .tp-leg-dist {
            font-family: var(--log-write);
            font-size: var(--fs-small);
            color: var(--log-ink-soft);
            white-space: nowrap;
        }
        #tripMapPage .tp-legs:active .tp-leg-said { color: var(--log-ink); }
        /* The note that the map cannot place a leg's meeting points: a sentence, so it wraps rather than being
           cut off by the leg line's ellipsis. */
        #tripMapPage .tp-leg-note { min-height: 0; padding-top: var(--sp-2xs); padding-bottom: var(--sp-2xs); }
        #tripMapPage .tp-leg-note .tp-leg-said {
            grid-column: 1 / -1;
            white-space: normal;
            overflow: visible;
            line-height: 1.4;
        }

        #tripMapPage .tp-leg-bar {
            width: 1px;
            height: 12px;
            background: var(--log-rule-firm);
        }

        #tripMapPage .tp-leg-mode {
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.14em;
            text-transform: uppercase;
            color: var(--log-ink-soft);
            white-space: nowrap;
        }
        #tripMapPage .tp-leg-mode.is-ask { color: var(--log-ink); }

        #tripMapPage .tp-leg-said {
            justify-self: start;
            max-width: 100%;
            min-width: 0;
            font-family: var(--log-write);
            font-size: var(--fs-small);
            color: var(--log-ink-soft);
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }

        /* TWO OF THE LEG’S RULES WERE HERE AND ARE GONE (2026-09-25).

           `.tp-leg-set` and `.tp-leg-open`, from cacdf22, when a leg in pieces was opened and set
           on the Stops sheet. e4eca28 tightened that — a leg is one press and its acts are in its
           own sheet — and these two stopped being emitted. Their two members went out of the
           shared `:active` list below as well, which css-dead does not report: it finds dead
           RULES, and a dead name inside a live list is a different search.

           ⚠️ THE REST OF THE FAMILY IS ALIVE. `.tp-leg-bar`, `-dist`, `-line`, `-mode`, `-note`,
           `-row` and `-said` are all built by the scripts. DEBTS §2 lists `.tp-leg-*` as drawn by
           no fixture, and that stays true — unphotographed is a different thing from dead. Only
           these two could never be drawn at all.

           `git show 35b599e:public/css/17j-trip-plan.css`. */


        /* ---- nights with nowhere to be ------------------------------------------- */
        /* The card's shape, dashed: this world's mark for a place kept rather than a record
           made. It sits on the same grid, so its words begin where the place names do. */
        #tripMapPage .tp-gap {
            grid-template-rows: auto auto;
            align-content: center;
            min-height: 72px;
            margin: var(--sp-xs) var(--sp-lg);
            padding: var(--sp-xs);
            border: 1px dashed var(--log-rule-firm);
            background: var(--log-stock);
        }
        /* ⚠️ THE GAP KEEPS ITS TWO LINES, AND THE REASON IS THE MEASURE. Squeezed onto
           one row beside its date and its offer, "5 NIGHTS WITH NOWHERE TO BE" had 186px to
           say 210px in — it wrapped, the date wrapped under it into three lines of two
           words, and the offer landed on top of them. Given the row to itself it fits, and
           the date and the offer share the line beneath. Two lines that fit beat one line
           that does not. */
        #tripMapPage .tp-gap-said {
            grid-column: 2 / -1;
            grid-row: 1;
            justify-self: start;
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.14em;
            text-transform: uppercase;
            color: var(--log-ink);
        }
        #tripMapPage .tp-gap-when {
            grid-column: 2;
            grid-row: 2;
            justify-self: start;
            margin: 2px 0 0;
            margin: 0;
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.12em;
            text-transform: uppercase;
            font-variant-numeric: lining-nums tabular-nums;
            color: var(--log-ink-soft);
        }
        #tripMapPage .tp-gap-add {
            grid-column: 3;
            grid-row: 2;
            justify-self: end;
            min-height: 44px;
            padding: 0;
            border: none;
            background: none;
            cursor: pointer;
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.16em;
            text-transform: uppercase;
            text-decoration: underline;
            text-underline-offset: 0.25em;
            text-decoration-thickness: 1px;
            color: var(--log-magenta);
            white-space: nowrap;
        }

        /* ---- the add line -------------------------------------------------------- */
        #tripMapPage .tp-add {
            min-height: 48px;
            cursor: pointer;
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.16em;
            text-transform: uppercase;
            text-decoration: underline;
            text-underline-offset: 0.25em;
            text-decoration-thickness: 1px;
            color: var(--log-magenta);
        }
        #tripMapPage .tp-add > span:last-child { grid-column: 2; }

        /* ---- the journey under way ----------------------------------------------- */
        /* The card the traveller is standing in. Not a tab hung off its edge — the plate's
           own number takes the ink, which is the mark this world gives a current state
           everywhere else. */
        /* The number tab is every stop's again (owner, 2026-09-19: 2可以恢复统一了): the HERE stamp says it, and over a
           photograph the ink-filled tab never did. The card's rule keeps the ink. */
        #tripMapPage .tp-row.is-now { border-color: var(--log-ink); }

        /* ---- the two ends -------------------------------------------------------- */
        /* They bracket the journey, so their heavy edges face outward and the ledger reads
           as one thing between them (owner: 应该改成上细下粗 这样更像一个整体). */
        #tripMapPage .tp-terminus {
            display: flex;
            align-items: baseline;
            justify-content: space-between;
            gap: var(--sp-sm);
            padding: var(--sp-xs) var(--sp-lg);
            background: var(--log-stock-band);
            border-top: 1px solid var(--log-rule-firm);
            border-bottom: 1px solid var(--log-rule-firm);
        }
        #tripMapPage .tp-terminus.is-begin {
            border-top-width: 2px;
            border-bottom-color: var(--log-rule);
        }
        #tripMapPage .tp-terminus.is-end {
            border-top-color: var(--log-rule);
            border-bottom-width: 2px;
        }
        #tripMapPage .tp-terminus-what {
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.18em;
            text-transform: uppercase;
            color: var(--log-ink);
        }
        #tripMapPage .tp-terminus-when {
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.12em;
            text-transform: uppercase;
            font-variant-numeric: lining-nums tabular-nums;
            color: var(--log-ink-soft);
        }

        /* ---- the narrow phone ---------------------------------------------------- */
        /* Its sizes — the 44px photograph, the rows at 60, the thread 22px in — became every phone's on
           2026-09-17, so it has nothing left to say. The note that stood here (a leg placed by a grid it was not
           written for, below 390) is kept in the history; the lesson holds: one grid, re-derived when it changes. */

        /* ---- the next line of the ledger ---------------------------------------- */
        /* THE ONE MAGENTA ON THIS SHEET. The colour is spent on the act that adds a line to
           the record, and on nothing else here: setting a leg's transport writes too, but it
           fills a column of a line already there, and five unset legs taking the colour five
           times would leave it meaning nothing.

           A full-width ruled row, not a button-shaped object — it is the place the next stop
           will go, and it reads as the empty line at the foot of a page. */
        #tripMapPage .tp-add {
            display: grid;
            grid-template-columns: 30px 1fr;
            gap: 0 var(--sp-sm);
            align-items: center;
            /* ⚠️ AUTO, NOT 100% (owner, 2026-09-13: stops 层是可以左右挪动的). The leg rules above give this line the
               legs' margin, 24px each side; 100% on top of that made it the sheet's width plus 24px, and the whole sheet
               scrolled sideways by exactly that. `tripHonest` asks that nothing on the sheet runs past the screen. */
            width: auto;
            box-sizing: border-box;
            min-height: 48px;
            padding: var(--sp-sm) var(--sp-lg);
            text-align: left;
            background: none;
            /* No rule under it (owner, 2026-09-17, circled in red): the button is only as wide
               as its words, so the rule stopped halfway across, a second line just above the
               terminus's own. */
            border: none;
            cursor: pointer;
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.16em;
            text-transform: uppercase;
            text-decoration: underline;
            text-underline-offset: 0.25em;
            text-decoration-thickness: 1px;
            color: var(--log-magenta);
        }
        #tripMapPage .tp-add:active { box-shadow: inset 0 -2px 0 0 var(--log-magenta); }

        /* ---- what the sheet says when it has nothing to show --------------------- */
        #tripMapPage .tp-quiet {
            margin: 0;
            padding: var(--sp-2xl) var(--sp-lg);
            font-family: var(--log-write);
            font-size: var(--fs-body);
            line-height: 1.5;
            color: var(--log-ink);
        }
        #tripMapPage .tp-quiet.is-reading { animation: wall-breathe 1.8s ease-in-out infinite; }
        /* Its own line under the sentence, not a word wedged into one: inside the
           paragraph the 44px target set that line's height and tore the sentence in two.
           The paragraph keeps its top inset and gives up its bottom one, so the two read
           as one block. */
        #tripMapPage .tp-quiet.is-failed { padding-bottom: 0; }
        #tripMapPage .tp-again {
            display: block;
            margin: 0;
            padding: 0 var(--sp-lg) var(--sp-2xl);
            min-height: 44px;
            text-align: left;
            background: none;
            border: none;
            cursor: pointer;
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.16em;
            text-transform: uppercase;
            text-decoration: underline;
            text-underline-offset: 0.25em;
            text-decoration-thickness: 1px;
            color: var(--log-ink);
        }

        /* ---- pressing, and the keyboard ----------------------------------------- */
        /* The Ink-Not-Glow Rule: a press thickens a baseline, it does not tint a ground. */
        #tripMapPage .tp-row:active { box-shadow: inset 0 -2px 0 0 var(--log-rule-firm); }
        #tripMapPage .tp-back:active,
        #tripMapPage .tp-act:active,
        #tripMapPage .tp-view:active,
        #tripMapPage .tp-again:active { box-shadow: inset 0 -2px 0 0 currentColor; }

        #tripMapPage :focus-visible {
            outline: 2px solid var(--log-ink);
            outline-offset: 2px;
        }

        /* ==========================================================================
           THE DAYS VIEW — the same ledger, read by day instead of by place
           --------------------------------------------------------------------------
           Migrated 2026-09-09. Drawn by renderItineraryDays() in public/02-state.js and
           mounted by renderTripTimelineTab() in public/05-timeline.js.

           WHAT IT REPLACES: a day header with a rotating chevron, and rows made of a
           coloured disc holding a category glyph (a 22-alpha wash computed inline from the
           category hex), a time, a name, and a tick for "booked". An empty day said
           "Thrilling new activities are ready to be unlocked!" — a marketing exclamation
           and a gamification metaphor, in a product whose recorded voice forbids both.

           It shares the Stops sheet's grammar on purpose: the same 30px margin, the same
           column rule at the same x, the same terminus-style band. Two readings of one
           journey should be one instrument turned over, not two instruments.
           ========================================================================== */

        /* The day's own band. Same construction as TRIP BEGINS: a printed label, a written
           account of it, and one word that acts. */
        #tripMapPage .dy-band {
            display: grid;
            grid-template-columns: auto 1fr auto;
            align-items: baseline;
            gap: var(--sp-sm);
            padding: var(--sp-xs) var(--sp-lg);
            background: var(--log-stock-band);
            border-top: 1px solid var(--log-rule-firm);
            border-bottom: 1px solid var(--log-rule-firm);
        }
        #tripMapPage .dy-n {
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.18em;
            text-transform: uppercase;
            color: var(--log-ink);
        }
        #tripMapPage .dy-when {
            font-family: var(--log-write);
            font-size: var(--fs-small);
            color: var(--log-ink-soft);
        }
        /* The fold, as a word. It was a chevron that rotated — a glyph, and a state you had
           to read off an angle. The handler in 02-state.js rewrites the word. */
        #tripMapPage .dy-fold {
            justify-self: end;
            min-height: 44px;
            padding: 0;
            background: none;
            border: none;
            cursor: pointer;
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.16em;
            text-transform: uppercase;
            text-decoration: underline;
            text-underline-offset: 0.25em;
            text-decoration-thickness: 1px;
            color: var(--log-ink-soft);
        }
        #tripMapPage .dy-fold:active { box-shadow: inset 0 -2px 0 0 currentColor; }

        /* Today's day carries the tab the Stops sheet carries at the station you are at —
           the same marker, at the same kind of place, so the two views agree about where
           you are. */
        #tripMapPage .dy-day.is-now .dy-n {
            background: var(--log-ink);
            color: var(--log-stock);
            padding: 3px 6px;
            margin: -3px 0;
        }

        #tripMapPage .dy-items.collapsed { display: none; }

        /* An entry in a day. The same grid as a stop's row, so the column rule lands on one
           x whichever view is open. */
        /* ⚠️ THE MARGIN IS AS WIDE AS THE PHONE'S TIME (owner, 2026-09-13: days 里面 时间被分割线压住了). It was 30px,
           which holds 06:15 and not 6:15 AM, still less 12:00 PM: on a phone that keeps a 12-hour clock every time ran
           under the thread. The row, the add line and the thread all read one width, so the thread still lands on one
           x down the whole day; `daysHonest` asks that every time fits inside it. */
        #tripMapPage { --dy-margin: 30px; }
        .clock-12h #tripMapPage { --dy-margin: 46px; }
        #tripMapPage .dy-row {
            position: relative;
            display: grid;
            grid-template-columns: var(--dy-margin) 1fr auto;
            align-items: baseline;
            gap: 0 var(--sp-sm);
            padding: var(--sp-sm) var(--sp-lg);
            width: 100%;
            box-sizing: border-box;
            text-align: left;
            font: inherit;
            background: none;
            border: none;
            border-bottom: 1px solid var(--log-rule);
            cursor: pointer;
        }

        /* THE CATEGORY COLOURS THE THREAD. The ten hues are the one colour PRODUCT.md allows
           in an achromatic interface, because they carry meaning — so they are spent on the
           line that is already structural rather than on a disc behind an icon. Every hue
           was chosen against this stock and clears 3:1 as a non-text mark; the palest,
           `others` #6B6D71, measures 3.84:1.

           2px rather than the ledger's 1px: it has to read as this entry's own segment of
           the thread and not as a tint of the rule. */
        #tripMapPage .dy-row::before {
            content: '';
            position: absolute;
            left: calc(var(--sp-lg) + var(--dy-margin) + (var(--sp-sm) / 2) - 1px);
            top: 0;
            bottom: 0;
            width: 2px;
            background: var(--dy-cat, var(--log-rule-firm));
        }

        /* The time is the day's own reference, so it takes the margin the stop number takes
           on the other view. Tabular, because a column of times that does not line up is a
           column of times you have to read twice. */
        #tripMapPage .dy-time {
            grid-column: 1;
            grid-row: 1;
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.04em;
            font-variant-numeric: lining-nums tabular-nums;
            color: var(--log-ink-soft);
            white-space: nowrap;
        }

        #tripMapPage .dy-what {
            grid-column: 2;
            grid-row: 1;
            font-family: var(--log-write);
            font-size: var(--fs-body);
            line-height: 1.3;
            color: var(--log-ink);
        }
        /* What Boswell wrote from a booking says so, printed by the log beside the place, not written into it (2026-09-17). */
        #tripMapPage .dy-mark {
            margin-left: var(--sp-xs);
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.12em;
            text-transform: uppercase;
            white-space: nowrap;
        }
        #tripMapPage .dy-where:empty { display: none; }
        #tripMapPage .dy-where {
            grid-column: 2 / 3;
            grid-row: 2;
            margin-top: 2px;
            font-family: var(--log-write);
            font-size: var(--fs-small);
            line-height: 1.4;
            color: var(--log-ink-soft);
        }

        /* The Stamped-State Rule. It was a tick glyph for booked and an empty slot for not,
           so the state a traveller most needs — the thing still to arrange — was said by the
           ABSENCE of a mark. Both states are printed now. NOT BOOKED is the one outstanding,
           so it takes the ink; BOOKED is settled and recedes.

           "To book" until 2026-09-09 (owner). The app said three things for one state — the
           stops sheet said NOT BOOKED, this said TO BOOK, the ticket said PENDING — and a
           traveller reading two of them has to work out whether they mean the same. NOT
           BOOKED is the one the stops sheet already used and the one that describes the
           record rather than instructing the reader. */
        #tripMapPage .dy-state {
            grid-column: 3;
            grid-row: 1;
            justify-self: end;
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.16em;
            text-transform: uppercase;
            color: var(--log-ink);
            white-space: nowrap;
        }
        #tripMapPage .dy-state.is-booked { color: var(--log-ink-soft); }

        #tripMapPage .dy-row:active { box-shadow: inset 0 -2px 0 0 var(--log-rule-firm); }

        /* A day with nothing arranged. On a long trip that is the point, not a gap. */
        #tripMapPage .dy-quiet {
            margin: 0;
            padding: var(--sp-md) var(--sp-lg);
            font-family: var(--log-write);
            font-size: var(--fs-small);
            line-height: 1.5;
            color: var(--log-ink-soft);
            border-bottom: 1px solid var(--log-rule);
        }
        #tripMapPage .dy-quiet.is-first {
            font-size: var(--fs-body);
            color: var(--log-ink);
            padding: var(--sp-2xl) var(--sp-lg);
            border-bottom: none;
        }

        /* The line that adds to a day. Same act, same colour and same place in the ledger as
           ADD A STOP on the other view: the magenta marks adding a LINE to the record, and
           the line sits where the thing it adds will land. It carries the empty margin so the
           thread runs through it, and it is drawn on every day including the empty ones —
           a day with nothing on it is exactly when you want the way to put something there. */
        #tripMapPage .dy-add {
            position: relative;
            display: grid;
            grid-template-columns: var(--dy-margin) 1fr;
            gap: 0 var(--sp-sm);
            align-items: center;
            width: 100%;
            box-sizing: border-box;
            min-height: 48px;
            padding: var(--sp-sm) var(--sp-lg);
            text-align: left;
            background: none;
            border: none;
            border-bottom: 1px solid var(--log-rule);
            cursor: pointer;
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.16em;
            text-transform: uppercase;
            text-decoration: underline;
            text-underline-offset: 0.25em;
            text-decoration-thickness: 1px;
            color: var(--log-magenta);
        }
        #tripMapPage .dy-add::before {
            content: '';
            position: absolute;
            left: calc(var(--sp-lg) + var(--dy-margin) + (var(--sp-sm) / 2));
            top: 0;
            bottom: 0;
            width: 1px;
            background: var(--log-rule-firm);
        }
        #tripMapPage .dy-add > span:last-child { grid-column: 2; }
        #tripMapPage .dy-add:active { box-shadow: inset 0 -2px 0 0 var(--log-magenta); }

        /* ==========================================================================
           PHOTOGRAPHS ON THE CHART
           --------------------------------------------------------------------------
           Reported 2026-09-09: the marks saying a place has photographs were so small you
           had to zoom in twice on a phone to find them — a 7px rust disc, with the
           photograph itself appearing only at zoom 9 or closer, which is never the zoom the
           trip map opens on.

           The owner's own answer, and the right one: do what the globe does, put the picture
           where there is room and run a line back to where it belongs. A chart has always
           been annotated that way, and it dissolves what the zoom threshold was working
           around — two points close together cannot both wear a photograph, but they can
           both point at one.
           ========================================================================== */

        /* Zero size, sitting exactly on the coordinate. Everything visible hangs off it, so
           what tracks the map is the point and not a picture's corner. */
        #tripMapPage .tm-pin {
            position: absolute;
            width: 0;
            height: 0;
            cursor: pointer;
        }

        /* The leader. A hairline in the firm rule, from the point outward — the same line
           weight the ledger's column rule uses, because it is doing the same job: saying
           that this belongs to that. */
        #tripMapPage .tm-pin-leader {
            position: absolute;
            left: 0;
            top: 0;
            width: var(--pin-len, 58px);
            height: 1px;
            transform-origin: 0 50%;
            transform: rotate(var(--pin-turn, -90deg));
            background: var(--log-rule-firm);
            /* On a pale tile a hairline alone can vanish, so it carries a stock outline —
               an OUTLINE and not a box-shadow, which is how the frame does it too. This
               world has no shadows, and a 1px halo written as a shadow is still a shadow in
               the stylesheet even when it looks like an outline on screen. */
            outline: 1px solid var(--log-stock);
        }

        /* The photograph, at the far end. Square, with a rule and a stock outline — the same
           frame the stop rows give a thumbnail, so a picture means the same thing on the
           sheet and on the chart. */
        #tripMapPage .tm-pin-frame {
            position: absolute;
            left: var(--pin-dx, 0);
            top: var(--pin-dy, -58px);
            transform: translate(-50%, -50%);
            display: block;
            width: 52px;
            height: 52px;
            background: var(--log-stock-band);
            border: 1px solid var(--log-rule-firm);
            outline: 1px solid var(--log-stock);
        }
        #tripMapPage .tm-pin-frame img {
            width: 100%;
            height: 100%;
            object-fit: cover;
            display: block;
        }

        /* How many photographs are here. Only when there is more than one — a "1" on every
           single-photograph pin is noise. Printed figures on the ink, knocked out in the
           stock: 8.93:1, and it reads as a stamp on the corner of the frame rather than a
           badge floating over it. */
        #tripMapPage .tm-pin-n {
            position: absolute;
            right: -1px;
            bottom: -1px;
            min-width: 16px;
            height: 15px;
            padding: 0 3px;
            display: flex;
            align-items: center;
            justify-content: center;
            background: var(--log-ink);
            color: var(--log-stock);
            font-family: var(--log-print);
            font-size: 10px;
            font-weight: 600;
            font-variant-numeric: lining-nums tabular-nums;
            line-height: 1;
        }

        #tripMapPage .tm-pin-frame:active { outline-color: var(--log-ink); }

        /* ==========================================================================
           #AJ-2  The sheet behind CHANGE — how one leg was travelled
           ==========================================================================

           Owner, 2026-09-12: change 还是旧世界. The word CHANGE was put on every leg of the
           plan yesterday so a wrongly-entered one could be corrected; the sheet it opened had
           never moved — a × for the way out, six modes drawn as SVG tiles, and the same six
           again as chips under a parenthetical that explained the difference.

           ⚠️ EVERY RULE IS QUALIFIED BY #transportPickerModal, for the reason 17q and 17s are:
           `.modal-content`, `.modal-body` and `.btn` are dressed in 05-interaction.css,
           23-legacy-plate.css and 24-tail.css, and two of the three load AFTER this file. An
           unqualified rule here would lose on order; an id-qualified one wins on specificity
           wherever it sits.

           The composition is the ruled list this world writes everything else on:

               a band naming the leg                LISBON – MARRAKESH        Close
               ─────────────────────────────────────────────────────────────────────
               FROM YOUR ITINERARY                  (only when there is one)
               TP1442  ·  06:15 · TP1442
               ─────────────────────────────────────────────────────────────────────
               OR MARK THE MODE
               a mark names the mode. a ticket keeps its number, time and cost.
               Flight                        Add ticket        MARKED
               Train                         Add ticket
               …
               Take this leg off

           Three columns, and the third is the Stamped-State column: empty on five rows, so
           the six Add tickets line up under each other and the one stamp sits outboard of
           them. A ledger's column, not a highlight. */

        #transportPickerModal .modal-content {
            /* Centred on the dim, where a leg's card and an activity's sit — it opens from one of them, and a
               sheet pinned to the top reads as a different kind of thing (owner, 2026-09-17: 层高不对). margin:auto
               rather than align-items, so a sheet taller than the screen still starts at its top and scrolls. */
            margin: auto 0;
            max-width: none;
            border-radius: 0;
            box-shadow: none;
            padding: 0;
            background: var(--log-stock);
        }

        /* The editor's round found this element carrying three separate insets at once;
           the margin column belongs to the rows inside, at one value. */
        #transportPickerModal .modal-body {
            padding: 0;
            margin: 0;
            width: auto;
            background: var(--log-stock);
        }

        /* ---- the band, which says WHICH leg -------------------------------------- */
        /* It said "Add Transport" on every leg of the journey, so pressing Change on the
           wrong one looked exactly like pressing it on the right one. */
        #transportPickerModal .tport-top {
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: var(--sp-md);
            min-height: var(--log-row);
            padding: var(--sp-2xs) var(--sp-lg);
            background: var(--log-stock-band);
            border-bottom: 1px solid var(--log-rule-firm);
        }

        #transportPickerModal .tport-hd {
            min-width: 0;
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.18em;
            text-transform: uppercase;
            color: var(--log-ink);
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }

        #transportPickerModal .tport-out {
            flex: none;
            min-height: 44px;
            padding: 0;
            border: none;
            border-radius: 0;
            background: none;
            cursor: pointer;
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.16em;
            text-transform: uppercase;
            text-decoration: underline;
            text-underline-offset: 0.25em;
            text-decoration-thickness: 1px;
            color: var(--log-ink);
        }

        /* ---- what names a section ------------------------------------------------ */
        #transportPickerModal .tport-label {
            padding: var(--sp-md) var(--sp-lg) var(--sp-2xs);
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.16em;
            text-transform: uppercase;
            color: var(--log-ink-soft);
        }

        /* One line of the ledger's own voice, in place of the "(no itinerary entry)" that
           was doing this work inside a heading. */
        #transportPickerModal .tport-note {
            padding: 0 var(--sp-lg) var(--sp-xs);
            font-family: var(--log-write);
            font-size: var(--fs-small);
            line-height: 1.4;
            color: var(--log-ink-soft);
        }

        /* ---- something already written in the itinerary --------------------------- */
        #transportPickerModal .tport-itin {
            display: block;
            width: 100%;
            box-sizing: border-box;
            min-height: 44px;
            padding: var(--sp-xs) var(--sp-lg);
            border: none;
            border-top: 1px solid var(--log-rule);
            border-radius: 0;
            background: none;
            text-align: left;
            cursor: pointer;
        }

        #transportPickerModal .tport-itin:active { background: var(--log-stock-band); }

        #transportPickerModal .tport-itin-nm {
            display: block;
            font-family: var(--log-write);
            font-size: var(--fs-body);
            line-height: 1.3;
            color: var(--log-ink);
        }

        #transportPickerModal .tport-itin-mt {
            display: block;
            margin-top: 1px;
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.1em;
            text-transform: uppercase;
            font-variant-numeric: lining-nums tabular-nums;
            color: var(--log-ink-soft);
        }

        /* ---- the modes, as one list ---------------------------------------------- */
        /* It was two: a tile grid that created a ticket and a chip row that did not. One
           question asked twice, in two shapes, with the answer to "what is the difference"
           in a parenthetical above the second.

           ⚠️ ONE GRID FOR ALL SIX ROWS, NOT SIX GRIDS. A grid's columns are shared by the
           rows of THAT grid and nothing else — so six per-row grids would have sized their
           third column six times over, and the five rows with no stamp would have pulled
           their Add ticket outboard of the one row that has one. The rows are
           display:contents and the columns live on the list. Which is also why every row
           renders its state cell even when it is empty: with contents, a row short of a cell
           lets the NEXT row's first child fall into the column it left open. */
        #transportPickerModal .tport-modes {
            display: grid;
            grid-template-columns: minmax(0, 1fr) auto auto;
            align-items: center;
            border-bottom: 1px solid var(--log-rule);
        }

        #transportPickerModal .tport-mode { display: contents; }

        /* The rule runs across all three cells rather than around the row, because with
           display:contents there is no row box to put a border on. No column gap, so the
           three hairlines meet and read as one ruling. */
        #transportPickerModal .tport-pick,
        #transportPickerModal .tport-keep,
        #transportPickerModal .tport-state { border-top: 1px solid var(--log-rule); }

        /* Pressing the line marks the leg — the whole reason most people open this sheet. */
        #transportPickerModal .tport-pick {
            min-height: 44px;
            padding: 0 var(--sp-md) 0 var(--sp-lg);
            border-left: none;
            border-right: none;
            border-bottom: none;
            border-radius: 0;
            background: none;
            text-align: left;
            cursor: pointer;
            font-family: var(--log-write);
            font-size: var(--fs-base);
            color: var(--log-ink);
        }

        #transportPickerModal .tport-pick:active { color: var(--log-magenta); }

        /* MAGENTA: it opens the activity form, and opening the way to add counts as adding
           (owner, 2026-09-17: 打开表单算写记录，删除统一用品红). It was ink, on the argument that only the form's SAVE writes. */
        #transportPickerModal .tport-keep {
            align-self: stretch;
            min-height: 44px;
            padding: 0;
            border-left: none;
            border-right: none;
            border-bottom: none;
            border-radius: 0;
            background: none;
            cursor: pointer;
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.16em;
            text-transform: uppercase;
            text-decoration: underline;
            text-underline-offset: 0.25em;
            text-decoration-thickness: 1px;
            color: var(--log-magenta);
            white-space: nowrap;
        }

        /* The state column. Sized by the one stamp in it, held open on the other five so
           the six Add tickets share a right edge. */
        #transportPickerModal .tport-state {
            align-self: stretch;
            display: flex;
            align-items: center;
            justify-content: flex-end;
            padding: 0 var(--sp-lg) 0 var(--sp-md);
        }

        /* Stamped-State: a word in a hairline box at the row's right, in print micro caps.
           MARKED for a mode on its own, TICKET where an itinerary entry is kept with it —
           which is the distinction the outgoing sheet spent a heading explaining. */
        #transportPickerModal .tport-stamp {
            padding: 2px var(--sp-xs);
            border: 1px solid var(--log-rule-firm);
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.14em;
            text-transform: uppercase;
            color: var(--log-ink);
            white-space: nowrap;
        }

        /* ---- taking it off ------------------------------------------------------- */
        /* Magenta, because this one does reach the record and remove from it. It was a grey
           .btn-secondary calling a function that does not exist. */
        /* ⚠️ AND IT HAS TO OBEY [hidden]. `display: block` on a class beats the user agent's
           [hidden] { display: none } on specificity, so the button showed on a leg with
           NOTHING to take off — 53px of magenta offering to remove what was never there.
           Found by asking the browser what it computed, not by looking at the picture: the
           element carried hidden=true the whole time and photographed as a control. This
           app has no global [hidden] guard, so every rule that gives a hidden element a
           display owes it this line. */
        #transportPickerModal .tport-drop[hidden] { display: none; }

        /* ---- a leg's card, on the stack like an activity's (see openLegSheet) ----- */
        /* The act-card's centring, dim and rise, in the transport sheet's own dress: the band with the leg's
           two ends and CLOSE, a ruled block per piece, and ADD A CONNECTION at the foot. */
        .act-card.leg-card {
            width: 90%;
            max-width: 360px;
            padding: 0;
            background: var(--log-stock);
            border: 1px solid var(--log-rule-firm);
            color: var(--log-ink);
        }
        .leg-card .leg-top {
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: var(--sp-md);
            min-height: var(--log-row);
            padding: 0 var(--sp-lg);
            background: var(--log-stock-band);
            border-bottom: 1px solid var(--log-rule-firm);
        }
        .leg-card .leg-hd {
            min-width: 0;
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.18em;
            text-transform: uppercase;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }
        /* A piece: the press that opens its ticket lies under the whole row, and CHANGE stands above it. */
        .leg-card .leg-piece {
            position: relative;
            padding: var(--sp-sm) var(--sp-lg) 0;
            border-bottom: 1px solid var(--log-rule);
        }
        .leg-card .leg-open {
            position: absolute;
            inset: 0;
            width: 100%;
            padding: 0;
            border: none;
            background: none;
            cursor: pointer;
        }
        .leg-card .leg-open:active { background: var(--log-stock-band); }
        .leg-card .leg-l1 {
            position: relative;
            pointer-events: none;
            display: grid;
            grid-template-columns: 56px minmax(0, 1fr);
            gap: var(--sp-sm);
            align-items: baseline;
        }
        .leg-card .leg-mode {
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.14em;
            text-transform: uppercase;
            color: var(--log-ink-soft);
        }
        /* ⚠️ TWO VOICES, NOT FIVE (owner, 2026-09-17: 字体乱七八糟的 统一一下). The piece's three lines were three sizes
           in two faces — the name at the base size, the places small, the times in the print face bold — so nothing
           lined up with anything. The labels and the acts (BUS, FLIGHT, CHANGE) keep the print caps every label and
           act in this world wears; every line of content is the write face at one size, and the order among them is
           said by weight and ink alone: what it is in full ink and medium, where and how long in the soft ink. */
        .leg-card .leg-said,
        .leg-card .leg-l2,
        .leg-card .leg-times {
            font-family: var(--log-write);
            font-size: var(--fs-small);
            line-height: 1.4;
            font-variant-numeric: lining-nums tabular-nums;
        }
        .leg-card .leg-said { font-weight: 500; }
        .leg-card .leg-min { color: var(--log-ink-soft); font-weight: 400; }
        /* The places and the times start where the words of the first line do. */
        .leg-card .leg-l2,
        .leg-card .leg-l3 {
            position: relative;
            margin-left: calc(56px + var(--sp-sm));
        }
        .leg-card .leg-l2 {
            pointer-events: none;
            color: var(--log-ink-soft);
        }
        .leg-card .leg-l3 {
            pointer-events: none;
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: var(--sp-md);
        }
        .leg-card .leg-l3 .leg-word { pointer-events: auto; }
        .leg-card .leg-times { pointer-events: none; }
        .leg-card .leg-foot { padding: 0 var(--sp-lg); }
        .leg-card .leg-word {
            min-height: 44px;
            padding: 0;
            border: none;
            background: none;
            cursor: pointer;
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.16em;
            text-transform: uppercase;
            text-decoration: underline;
            text-underline-offset: 0.25em;
            text-decoration-thickness: 1px;
            color: var(--log-ink);
        }

        #transportPickerModal .tport-drop {
            display: block;
            width: 100%;
            box-sizing: border-box;
            min-height: var(--log-row);
            margin: 0;
            padding: 0 var(--sp-lg);
            border: none;
            border-top: 1px solid var(--log-rule-firm);
            border-radius: 0;
            background: none;
            text-align: left;
            cursor: pointer;
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.16em;
            text-transform: uppercase;
            text-decoration: underline;
            text-underline-offset: 0.25em;
            text-decoration-thickness: 1px;
            color: var(--log-magenta);
        }

        /* A travel mate reads the nights and does not step them (planIsOwner, 04-stops.js): one column, where three were,
           with the figure over its word (owner, 2026-09-20: 数字跟nights居中) — the count names column 2, which in a grid of
           one is a column of its own to the right of NIGHTS. */
        #tripMapPage .tp-nights.is-read { grid-template-columns: auto; }
        #tripMapPage .tp-nights.is-read .tp-count { grid-column: 1; }

        /* ---- what is around here (2026-09-20 under the plate, 2026-09-21 on it) ---------------- */
        /* A square strip of five marks over the chart, and a line under it that says what was pressed and what came
           of it (owner: 放在地图上方 用图标标是 小屏手机友好 只是胶囊用直角的).

           It was five words on a rule under the plate for a day, which is where this world would put it — but in
           this world's caps those five words measure about 460px and a phone is 375, so they wrapped to three lines
           and took a third of the map on exactly the screen the map is for. As marks they are one row of five, 240px,
           and they sit ON the chart rather than eating it.

           What makes that admissible on a page that threw two floating buttons off itself in September: they are
           about what is under them, they are square (the reference app's pill is the outgoing world's shape and the
           owner asked for the corners back), and the line below names the pressed one in words — so nothing here is
           an unlabelled glyph for long. */
        #tripMapPage .tp-near { display: none; }
        #tripMapPage.is-map .tp-near {
            display: block;
            position: absolute;
            top: var(--sp-sm);
            left: 50%;
            transform: translateX(-50%);
            z-index: 2;
            width: max-content;
            max-width: calc(100% - 2 * var(--sp-md));
            text-align: center;
            pointer-events: none;          /* the map is still draggable between the marks */
        }
        #tripMapPage .tp-near[hidden] { display: none; }

        #tripMapPage .tp-near-words {
            display: inline-flex;
            pointer-events: auto;
            background: var(--log-stock);
            border: 1px solid var(--log-rule-firm);
        }
        #tripMapPage .tp-near-mark {
            /* 44 across for the thumb, 36 down for the eye: the strip should read as a small instrument, not a
               toolbar (owner, 2026-09-21: 不够精致). */
            width: 44px;
            height: 36px;
            display: flex;
            align-items: center;
            justify-content: center;
            padding: 0;
            background: none;
            border: none;
            cursor: pointer;
            color: var(--log-ink-soft);
        }
        /* No rules between them (owner, 2026-09-21: 图标之间不要有分割线 胶囊外面有就行了): one box round the five, and
           inside it the marks stand on the stock with nothing between. This world rules a list of rows because each
           row is a record; five marks on one strip are one control, not five records. */
        /* Stamped, not filled: the mark takes the magenta and the rule under it closes, as a pressed word does. */
        #tripMapPage .tp-near-mark.is-on {
            color: var(--log-magenta);
            box-shadow: inset 0 -2px 0 0 var(--log-magenta);
        }

        #tripMapPage .tp-near-said {
            /* On its own line under the strip. Inline-block put it BESIDE the marks, which at 375 ran the whole
               notice off the screen (2026-09-21). Block, sized to its words, centred under them. */
            display: block;
            width: max-content;
            max-width: 100%;
            margin-inline: auto;
            pointer-events: auto;
            margin-top: var(--sp-2xs);
            padding: 3px var(--sp-sm);
            background: var(--log-stock);
            border: 1px solid var(--log-rule-firm);
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.12em;
            text-transform: uppercase;
            color: var(--log-ink);
        }
        #tripMapPage .tp-near-said:empty { display: none; }

        /* A found place on the kept map: the same mark Google's map draws for one, in the magenta, ringed in the
           paper so it reads on a street as on water. The stops stay as they are — numbered, in the ink. */
        #tripMapPage .nb-pin {
            width: 14px;
            height: 14px;
            box-sizing: border-box;
            border: 2px solid #FBF7EC;
            border-radius: 50%;   /* gate:allow round — 地图上的一个点，方块会读成图钉或标签，不是这个世界的圆角装饰 */
            background: var(--log-magenta);
            cursor: pointer;
        }
        #tripMapPage .maplibregl-popup-content {
            padding: 0;
            border: 1px solid var(--log-rule-firm);
            border-radius: 0;
            background: var(--log-stock);
            box-shadow: none;
        }
        #tripMapPage .maplibregl-popup-tip { display: none; }
