        /* ==========================================================================
           #A  Sign-in & registration — "the deck log"
           ==========================================================================

           These two screens no longer share the Grand Hotel world the rest of the app
           still carries. They were redesigned 2026-09-03 as a ship's deck log: one
           ruled instrument where a passage is planned, kept, and never erased.

           The palette is taken from what a deck log is actually made of, not from what
           "a travel journal" suggests. Admiralty chart stock is a cool blue-grey, ship's
           logs were written in indelible copying pencil (which is violet, not brown),
           the cover is black oilskin, and the only two colours a bridge recognises are
           port red and starboard green. Nothing here is cream, parchment or ivory —
           that was the outgoing world and it is the direction this replaces.

           Scoped to .login-page / .register-page on purpose. The other 24 screens are
           unconverted, so these tokens must not leak into them; when the rest of the
           app migrates, they graduate to :root. */

        .login-page,
        .register-page,
        #mainPage,
        #statsPage,
        #photoWallPage,
        #shelfPage,
        #profilePage,
        #profileEditPage,
        #boswellPage,
        #savedPage,
        #notificationsPage,
        #friendsPage,
        #worldMapPage,
        /* The journey's own room, joined 2026-09-09. It was the last big surface outside
           this list: the six trip files used --log-* zero times between them, because
           nothing they wrote could have resolved. */
        #tripMapPage,
        #toolkitPage,
        #syncPage,
        #settingsPage,
        #passwordPage,
        #aboutPage,
        #colophonPage,
        #helpPage,
        #legalPage,
        /* The toast container sits in .app-frame outside every page and is shown on
           all of them; it takes the world's tokens in its own right. */
        #toastContainer,
        /* The sync bar is appended to document.body, outside .app-frame, so it inherits
           nothing from any page. It named --log-* tokens it could never have resolved
           until it was put on this list (2026-09-08 finish review). */
        .sync-status-bar,
        /* The bar is fixed to the viewport and therefore lives outside .app-frame, which
           carries a transform; it takes the world's tokens in its own right, as the toast
           does. Joined 2026-09-07, when it was migrated out of the outgoing world. */
        .bottom-nav,
        /* The drawer and its scrim are inside .app-frame but inside no page, so they are
           named here for the same reason the toast is. Joined 2026-09-07. */
        .sidebar,
        .sidebar-overlay,
        /* The portrait opened from a profile (2026-09-20): on <body>, like every overlay, and its CLOSE named
           --log-stock — which, off this list, resolved to nothing and printed in the inherited ink on the night plate.
           The trap this list's own note describes, found by the owner on the phone: close看不到. */
        .pf-portrait,
        /* Overlays are appended to <body>, not to the page that opens them — .app-frame
           carries a transform, which would turn position:fixed into absolute — so each
           needs the tokens named here in its own right or every var() inside it resolves
           to nothing. */
        #addCountrySheet,
        #pinSheet,
        #savedSheet,
        #addFriendModal,
        /* The confirm and prompt dialogs are appended here. It sat inside .app-frame until
           2026-09-11 and now sits on <body> — the frame's desktop transform is a stacking
           context, and a dialog inside it was painted behind the page stack outside it. It
           is inside no page either way, so like the drawer it must be named or every var() in
           a dialog resolves to nothing — which is how those two stayed in the outgoing world's
           clothes long after everything around them had moved. Joined 2026-09-08. */
        #confirmDialogRoot,
        /* THE PAGE STACK, joined 2026-09-09 — the fourth surface this list has swallowed,
           after the sync bar, the drawer and the dialog root.

           .pgstack-root is appended to document.body, outside .app-frame, so every var() on
           every page it carries resolved to nothing. Nine surfaces live on it: the activity
           detail, the checklist, an expense, a journal entry, its photographs, a photograph
           full-screen, a day, and the weather. None of them had reached for a --log-* value
           yet, so nothing was broken in production — this was found by checking before
           writing rather than by shipping and being told. */
        .pgstack-root,
        /* ⚠️ THE TRIP'S SECTION SHEET, joined 2026-09-11 with the journal's list. It is a
           SIBLING of the pages, not a child of one — it has to be, because .app-frame
           carries a transform and position:fixed degrades inside a transformed ancestor —
           so every var() written inside it resolved to nothing, exactly as the dialogs and
           the page stack did before they were named here. Checked before writing rather
           than after shipping: `getComputedStyle(#tripSectionSheet)` returned an empty
           string for --log-ink while #mainPage returned #43276B.

           Five rooms live on this sheet — Calendar, Expenses, Gallery, Journal,
           Attachments — and naming it here paints none of them. Only the journal reaches
           for these values today. */
        /* ⚠️ THE TRIP MENU, joined 2026-09-14 with its migration. Its first photograph came back transparent: no
           ground, no rules, rows collapsed to their words, because every var(--log-*) in css/17zc-trip-menu.css resolved
           to nothing. The drawer is an overlay aligned to the frame, not a part of #tripMapPage's box, the same trap the
           section sheet, the sidebar and the dialogs each fell into. */
        #tripDetailsDrawer,
        #tripDetailsBackdrop,
        #tripSectionSheet,
        #tripSectionSheetBackdrop,
        /* THE LEFTOVERS, joined 2026-09-14 with their migration (css/17zd-leftovers.css): the two waiting overlays, the card
           a photo pin opens and the page an old share link opens. All four hang off <body>. */
        #loadingOverlay,
        #uploadProgressOverlay,
        #photoPlaceCard,
        #sharedTripOverlay,
        /* The journal's editor, joined 2026-09-11 with the third of the journal's rounds.
           A `.modal` in index.html, a sibling of the pages like every other overlay here,
           so every var() written inside it resolved to an empty string. Checked before
           writing, as the sheet was: getComputedStyle(#writeJournalModal) returned "" for
           --log-ink. Third surface in three days to need this line. */
        #writeJournalModal,
        /* And the sheet the editor's CHOOSE opens — same day, same trap. */
        #journalLinkModal,
        /* The photograph's activity picker, the same sheet since 2026-09-14. */
        #photoActivityModal,
        #diaryDraftsModal,
        #mediaPickerModal,
        /* ⚠️ ADD A TRIP, joined 2026-09-11 — the seventh surface this list has swallowed,
           and the seventh time the check was worth doing before writing a line of CSS:
           getComputedStyle(#planTripModal) returned "" for --log-ink while #mainPage
           returned #43276B. It is a `.modal` in index.html, a sibling of the pages, so
           nothing inside it could ever have resolved a --log-* value.

           This is also the room where a journey is FIRST WRITTEN, which makes it the one
           surface a new reader meets before any other in the trip world. It had stayed in
           the outgoing world the whole time the rooms it leads to were migrated. */
        #planTripModal,
        /* The date picker, written the same day. It is appended to <body> for the frame's
           transform (see #confirmDialogRoot above), and it is the first control in this
           app that draws a month WITHOUT being a page — so it owns no page's tokens and
           must be named here or every rule in css/17r-datepicker.css resolves to nothing. */
        #datePickerRoot,
        /* ⚠️ THE STOP SHEET, joined 2026-09-11 — the EIGHTH surface this list has swallowed,
           and the eighth time the check was worth doing before writing a line of CSS:
           getComputedStyle(#stopDetailSheet) returned "" for both --log-ink and --log-stock.
           It is a sibling of the pages on <body> (it is already in alignOverlaysToFrame for
           the same reason), so nothing inside it could ever have resolved a --log-* value.

           Eight is no longer a run of bad luck. Every overlay this app has is on <body>,
           because .app-frame's desktop transform degrades position:fixed — so EVERY overlay
           needs this line, and the list is the first place to look when a room refuses to
           take its colours. */
        #stopDetailSheet,
        /* ⚠️ THE TRANSPORT PICKER, joined 2026-09-12 — the NINTH, and checked before a line
           of CSS was written for it, as the eight before it were: getComputedStyle of
           #transportPickerModal returned "" for both --log-ink and --log-stock while
           #mainPage returned #43276B. It is a .modal in index.html, a sibling of the pages,
           so nothing inside it could ever have resolved a --log-* value. */
        #transportPickerModal,
        /* ⚠️ THE EXPENSE FORM, joined 2026-09-12 — the TENTH, and checked before a line of
           CSS as the nine before it were: getComputedStyle of #expenseModal returned "" for
           --log-ink while #mainPage returned #43276B. A .modal in index.html, a sibling of
           the pages. The list is still the first place to look when a room refuses its
           colours. */
        #expenseModal,
        /* ⚠️ THE ACTIVITY FORM, joined 2026-09-13 — the eleventh. Another .modal in index.html, a
           sibling of the pages, which nothing inside could ever have resolved a --log-* value in. */
        #addActivityModal,
        #sightsNotesExpandModal,
        #wallView {
            /* Surfaces */
            --log-stock: #D9DFDD;        /* chart stock — the ruled sheet */
            --log-stock-band: #CFD7D5;   /* the sheet's own heading band */
            --log-oilskin: #16191A;      /* cover boards; also the primary control */
            /* The night plate — Home's globe, the Wall, the photograph opened alone — and what
               is written on it. Used in 26 places since 2026-09-07 and declared nowhere until
               2026-09-17 (critique P1): every use carried these same two values as a fallback,
               so the colour was right by repetition, and one fallback edited alone would have
               drifted without a word. */
            --log-night-plate: #0B1220;
            --log-on-night-plate: #DCE9F8;

            /* Rules. Two weights, and the distinction is functional, not decorative:
               --log-rule is the hairline between rows and never carries meaning on its
               own; --log-rule-firm draws the column rules and entry baselines, which do,
               so it is held at 3.30:1 against the stock. */
            --log-rule: #9FAAA7;
            --log-rule-firm: #6E7A77;

            /* The line that has the cursor. Ink-Not-Glow says focus thickens a baseline and
               never tints a ring, and every writing line in this world also lifts its ground
               a shade while it is being written in. That pairing — this ground plus
               `inset 0 -2px 0 0 var(--log-ink)` — was written out **ten times in eight files**
               (the sign-in entry, Boswell's line and its editing row, Saved's search, both of
               My friends', the tool kit, Settings and the password form, Help, and the confirm
               dialog's input) as a bare literal, with no name and three comments describing it
               as if it were one. Found by `npm run capture -- --legacy`, 2026-09-21.
               ⚠️ It is LIGHTER than the stock, not the band: two of those comments call it
               "the band ground", and the band (--log-stock-band) is darker. A line being
               written in lifts toward the light; it does not sink toward the heading. */
            --log-stock-lit: #DEE4E2;

            /* Ink. One ink, two pressures — 8.94:1 and 5.06:1 on the stock.
               The light pressure was #6B5490, measured on the stock at 4.72:1 and never
               rechecked when the Register put whole sentences of it on the heading band:
               on #CFD7D5 the same ink is 4.35:1, under the floor, and what it was carrying
               was every line that says what a figure means and the whole colophon.
               Darkened until it clears on both grounds — 4.66:1 on the band, 5.06:1 on the
               stock — rather than promoted to full ink, because the softer voice under the
               figure is the hierarchy. */
            --log-ink: #43276B;
            --log-ink-soft: #665089;
            --log-on-oilskin: #E8EDEB;   /* 14.9:1 on the band */

            /* State. Port and starboard are the whole vocabulary; magenta is the
               chart's annotation overprint. All three are darkened from their signal
               values to clear 4.5:1 on the stock — 4.51, 4.86 and 5.06 respectively. */
            --log-port: #C4111F;
            --log-starboard: #006B49;
            --log-magenta: #A81E6E;

            /* Archivo Narrow is the log's printed voice: the ruling, the column heads,
               the labels stamped on the form. Hanken Grotesk (already loaded for the
               rest of the app) is the written voice — what the traveller puts in.
               A deck log has no display face; its authority is the ruling, so no display
               face was added. */
            /* ⚠️ 'CJK Sans Adj' JOINED BOTH FACES ON 2026-09-24, and it should have been here from
               the day they were written. It is an @font-face in 01-tokens.css that takes only the
               CJK code points, by unicode-range, from a named and ordered list of system faces —
               PingFang SC, Hiragino Sans GB, Microsoft YaHei, Noto Sans CJK SC — each carrying a
               size-adjust of 93% measured so a Chinese glyph's face height matches the Latin cap
               height beside it. Nothing is downloaded; the OS still chooses, but from that list.

               Until today it was named ONLY in --font-serif and --font-sans, the outgoing world's
               two. So 651 uses of the faces below — 91% of the type in this app — set Chinese
               through a bare system-ui with no adjustment, while 61 legacy rules got the tuned
               one. DESIGN.md:1078 describes the fallback as though it covered the app; it covered
               the part that was on its way out. Found while converting those 61, because doing it
               naively would have taken the adjustment AWAY from the last rules that had it.

               THE POSITION IS THE WHOLE THING (01-tokens.css:188): after the Latin face, before
               the system fallback. First and it never matches, having no Latin glyphs; last and
               the system fallback has already answered. */
            --log-print: 'Archivo Narrow', 'Hanken Grotesk', 'CJK Sans Adj', system-ui, sans-serif;
            --log-write: 'Hanken Grotesk', 'CJK Sans Adj', system-ui, sans-serif;
            /* --log-type is left alone: Courier Prime has no CJK, and what a Chinese glyph should
               look like in Boswell's typewriter voice is a decision nobody has made. */
            /* Boswell's own hand (owner, 2026-09-19): a third voice beside print and write, for what Boswell says. */
            --log-type: 'Courier Prime', 'Courier New', ui-monospace, monospace;

            /* The ruling pitch. One value, so an entry row and an unwritten row below it
               are the same height and the sheet reads as continuously ruled. */
            --log-row: 53px;
        }

        /* ---- The sheet ------------------------------------------------------- */

        .page.login-page.active,
        .page.register-page.active {
            display: flex;
        }

        .login-page,
        .register-page {
            position: fixed;
            inset: 0;
            width: 100%;
            height: 100%;
            z-index: 10000;
            background: var(--log-stock);
            /* Stretch, not centre. The outgoing screen floated a short form in the
               middle of the viewport with dead space above and below it; a log sheet
               is the page, edge to edge. */
            align-items: stretch;
            justify-content: flex-start;
            flex-direction: column;
            overflow-y: auto;
            -webkit-overflow-scrolling: touch;
        }

        .log-sheet {
            display: flex;
            flex-direction: column;
            min-height: 100%;
            width: 100%;
        }

        /* ---- Cover band ------------------------------------------------------ */

        .log-band {
            background: var(--log-oilskin);
            color: var(--log-on-oilskin);
            padding: calc(var(--sp-md) + env(safe-area-inset-top, 0px)) var(--sp-lg) var(--sp-md);
            /* The wordmark sizes against this band, not the viewport — see .log-mark. */
            container-type: inline-size;
        }

        /* What the instrument records. Without this the screen never contains the word
           travel, trip or journey: JOURNAL is inside a proper name and PLAN IT / LIVE IT
           / KEEP IT is a rhythm with no object, so a stranger could not tell a trip log
           from a diary or a ledger. A deck log carries its subject on the cover. */
        .log-subject {
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.16em;
            text-transform: uppercase;
            color: var(--log-on-oilskin);
            opacity: 0.82;
            margin: var(--sp-xs) 0 0;
            line-height: 1.4;
            /* Below ~360px this wraps, and unbalanced it drops ARCHIVE alone onto the
               second line. Balancing splits it evenly instead of leaving an orphan. */
            text-wrap: balance;
        }

        .log-mark {
            font-family: var(--log-print);
            /* One element is allowed to be enormous, and on this screen it is the name
               stamped on the cover.

               Sized in cqi against .log-band, not in vw. Above 768px the frame stops at
               430px while the viewport keeps growing, so a vw-based clamp kept enlarging
               the name inside a box that had stopped — at tablet width it reached exactly
               382px inside a 382px band, zero slack, and any fallback face overran it.

               The old nowrap turned that overrun into an overflow off the black band.
               Archivo Narrow arrives over the network and this app is built to work
               without one, so the name now wraps to two balanced lines in a fallback face
               instead. With the real face it stays on one line at every width. */
            font-size: 2rem;
            font-size: clamp(1.55rem, 9.9cqi, 2.4rem);
            font-weight: 700;
            letter-spacing: 0.06em;
            text-transform: uppercase;
            line-height: 1.05;
            margin: 0;
            color: inherit;
            text-wrap: balance;
        }

        /* ---- Column heads: the tagline, set as the log's three columns -------- */

        /* Three equal heads, in sequence, with no cell fill and no dividing rules.
           They were bordered cells sitting on the band's edge, which is the anatomy of a
           segmented control — and they are inert, and they head no columns, because the
           form below is a margin/entry grid. Equal weight and left-to-right order are
           what the product actually needs from them; the box was never load-bearing. */
        .log-phases {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            border-bottom: 1px solid var(--log-rule-firm);
        }

        .log-phases span {
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.16em;
            text-transform: uppercase;
            color: var(--log-ink);
            text-align: center;
            padding: var(--sp-md) var(--sp-2xs);
        }



        /* ---- Ruled entries --------------------------------------------------- */

        .log-entries {
            border-bottom: 1px solid var(--log-rule-firm);
        }

        .log-entry {
            display: grid;
            /* The label lives in the sheet's left margin column, on the same baseline
               as its value — a log rules its rows, it does not stack a caption above
               each one. This also removes the old defect where the mobile field text
               sat 16px right of the label that named it. */
            grid-template-columns: 6.5rem 1fr auto;
            align-items: stretch;
            position: relative;
        }

        .log-entry + .log-entry {
            border-top: 1px solid var(--log-rule);
        }

        .log-entry-label {
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.14em;
            text-transform: uppercase;
            color: var(--log-ink);
            /* The margin rule. Drawn once per sheet by the first entry and inherited
               down the column, so the rule reads as continuous rather than as a stack
               of per-row borders. */
            border-right: 1px solid var(--log-rule-firm);
            display: flex;
            align-items: center;
            padding: var(--sp-md) var(--sp-sm);
            background: var(--log-stock-band);
            min-height: 44px;
            line-height: 1.25;
        }

        .log-entry-value {
            font-family: var(--log-write);
            font-size: var(--fs-base);
            font-weight: var(--fw-regular);
            color: var(--log-ink);
            background: transparent;
            border: none;
            outline: none;
            width: 100%;
            min-height: 44px;
            padding: var(--sp-md) var(--sp-sm);
            caret-color: var(--log-ink);
            letter-spacing: 0.01em;
        }

        .log-entry-value::placeholder {
            color: var(--log-ink-soft);
            opacity: 1;
        }

        /* ══════════════════════════════════════════════════════════════════════════
           A REMEMBERED VALUE IS STILL THIS APP'S PAPER · 2026-09-12
           ══════════════════════════════════════════════════════════════════════════

           The owner, of the Add-a-stop form: 文字输入框 输入后 输入框背景会变颜色 不止一处
           是这样 这里是不是不对 是旧代码? Not old code — nothing in this app paints a field
           once it has a value. It is the BROWSER. When Chrome fills a field from what it
           remembers it washes it in its own pale blue (#E8F0FE), and it will not take a
           plain `background-color` from the page: the only thing that covers it is an inset
           box-shadow large enough to fill the box.

           ⚠️ AND THIS APP ALREADY KNEW, IN EXACTLY ONE FIELD OUT OF EIGHTY-ONE. Saved's
           search box has carried the fix since it was migrated, with the right note beside
           it — "the browser's own fill for a remembered value is not this page's" — and it
           was never generalised. Every other field in the app takes the wash: the stop's
           name, every field on the activity form, the expense form, sign-in, the journal.

           ⚠️ WHICH IS WHY THE GROUND IS A PROPERTY AND NOT A COLOUR. Those eighty-one
           fields sit on EIGHT different papers — measured, not guessed: 22 on --log-stock,
           36 on --surface-sheet in the activity form's cards, 5 on --bg-secondary, 3 on
           --surface-field, 2 on --log-stock-band, one on --bg-primary, and seven on a
           translucent scrim. A single hard-coded value would have been wrong for fifty-four
           of them, which is the fault this rule exists to remove, reintroduced by the rule.

           So: one rule, one property, defaulted to this world's stock, and set beside every
           other paper below. A room that changes its paper changes the property in the same
           place, which is the only arrangement that stays true.

           The transition is the second half of the trick and it is not decoration: Chrome
           re-applies the wash with a transition of its own, so without a very long one of
           ours the blue flashes through before the shadow lands. */
        input:-webkit-autofill,
        textarea:-webkit-autofill,
        select:-webkit-autofill,
        input:-webkit-autofill:hover,
        input:-webkit-autofill:active,
        textarea:-webkit-autofill:hover {
            -webkit-box-shadow: inset 0 0 0 1000px var(--fill-ground, #D9DFDD);
            -webkit-text-fill-color: var(--fill-ink, #43276B);
            caret-color: var(--fill-ink, #43276B);
            transition: background-color 9999s ease-out;
        }
        /* Focused, the field still shows the baseline that says so — the shadow that hides
           the wash would otherwise hide the mark as well. */
        input:-webkit-autofill:focus,
        textarea:-webkit-autofill:focus {
            -webkit-box-shadow: inset 0 0 0 1000px var(--fill-ground, #D9DFDD),
                                inset 0 -2px 0 0 var(--fill-ink, #43276B);
            -webkit-text-fill-color: var(--fill-ink, #43276B);
            caret-color: var(--fill-ink, #43276B);
            transition: background-color 9999s ease-out;
        }

        /* ── where the paper is not this world's stock ─────────────────────────

           ⚠️ EVERY SELECTOR HERE IS THE ELEMENT THAT ACTUALLY PAINTS THE GROUND, FOUND BY
           WALKING UP FROM ALL EIGHTY-ONE FIELDS AND READING THE FIRST OPAQUE BACKGROUND —
           not by reasoning about which room a field is in. The first draft of this list did
           reason, and it put the outgoing paper under sixteen fields that had already moved
           into the deck log: `.modal-content` is a class shared by modals on BOTH papers,
           so Add a trip and the journal's editor would have been painted #F0EDE7 on stock.
           The three named below are the three still on the old paper.

           When a room moves into the deck log its line here goes with it rather than being
           left behind painting a paper the room no longer uses.

           ⚠️ THAT IS EXACTLY WHAT HAPPENED (critique P1, 2026-09-16; removed 2026-09-17). The
           activity form's five cards, the expense form, its currency search and the link box
           moved onto the stock, and their four lines stayed here declaring the outgoing
           papers — 36 of 74 fields would have filled a remembered value in parchment on
           stock. `signin/mobile.png` said so on every run from 2026-09-14 on. One outgoing
           paper is left, below; a room on a paper of its own in the deck log declares it in
           its own file (the viewer: 17y-viewer.css). */
        /* THE LAST OUTGOING PAPER, and it is gone (2026-09-24). The block above said so of
           itself — "One outgoing paper is left, below" — and this was it: the activity form’s
           currency menu filled a remembered value in --surface-field on --text-primary, the
           retired near-white and near-black, while the expense form’s filled on the stock.
           Both fill on the stock now.

           It survived this long because it is only visible when the BROWSER fills a field, which
           no fixture does and no photograph has ever caught. It was found by grepping for
           `var(--fill-ground)` and finding nothing — then noticing that every real reader
           writes `var(--fill-ground, #D9DFDD)`, with a fallback, which an exact-string search misses. */
        #activityCurrencyMenu .cur-search {
            --fill-ground: var(--log-stock);
            --fill-ink: var(--log-ink);
        }
        /* On the deck log, but on its band rather than its stock: the two place searches,
           and the trip cover's own bar — whose link field sits on the hero's band. */
        .country-search, .ntrip-hero { --fill-ground: var(--log-stock-band, #CFD7D5); }

        .log-entry[data-fault="true"] {
            box-shadow: inset 0 -2px 0 0 var(--log-port);
        }

        .log-entry[data-fault="true"] .log-entry-label {
            color: var(--log-port);
        }

        /* Focus is a mark on the sheet, not a glow: the entry's own baseline thickens
           into ink. Hover carries nothing — the Capacitor build has no pointer. */
        .log-entry:focus-within {
            background: var(--log-stock-lit, #DEE4E2);
            box-shadow: inset 0 -2px 0 0 var(--log-ink);
        }

        .log-entry-value:focus-visible {
            outline: none;
        }

        /* ---- Reveal: the log's own correction mark ---------------------------- */
        /* No eye icon. In a log an instruction that has been carried out is struck
           through, never wiped, so the control strikes itself when the password is
           showing. aria-pressed carries the state for anyone not reading the strike. */

        /* Edge draws its own eye inside every password field, beside the word this
           world chose instead of an eye — two reveals for one password, one of them in
           the outgoing world's idiom. Chrome has no such control, which is why it was
           never seen here. Off, in every scope this sheet's tokens reach. */
        .login-page input::-ms-reveal,
        .login-page input::-ms-clear,
        .register-page input::-ms-reveal,
        .register-page input::-ms-clear,
        #profileEditPage input::-ms-reveal,
        #profileEditPage input::-ms-clear {
            display: none;
        }

        .log-reveal {
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.14em;
            text-transform: uppercase;
            color: var(--log-ink);
            background: transparent;
            border: none;
            border-left: 1px solid var(--log-rule);
            padding: 0 var(--sp-md);
            min-height: 44px;
            min-width: 44px;
            cursor: pointer;
            text-decoration-color: var(--log-ink);
            text-decoration-thickness: 1px;
            text-underline-offset: 0.2em;
        }

        .log-reveal[aria-pressed="true"] {
            text-decoration-line: line-through;
            color: var(--log-magenta);
            text-decoration-color: var(--log-magenta);
            /* Held down, not struck out. Without a seated background the struck word
               reads as the disabled idiom and the second press is undiscoverable. */
            background: var(--log-stock-band);
            box-shadow: inset 0 -2px 0 0 var(--log-magenta);
        }

        /* ---- Marginalia: forgot-password, and the ruled note line ------------- */

        .log-note {
            display: flex;
            justify-content: flex-end;
            align-items: center;
            padding: 0 var(--sp-lg);
            min-height: var(--log-row);
            border-bottom: 1px solid var(--log-rule);
        }

        /* ---- forgotten your password ------------------------------------------------ */

        /* Content, not a control. See the note in 30-markup-auth.js: this was a magenta
           link that raised a "coming soon" toast, and there is no recovery behind it to
           reach. The row keeps its place on the sheet so the question is answered where it
           is asked, and answers it with a stamp. */
        .log-note.is-stamped {
            justify-content: space-between;
            gap: var(--sp-sm);
        }

        .log-note-what {
            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);
        }

        .log-note-stamp {
            flex: none;
            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);
            border: 1px solid var(--log-rule);
            padding: 2px var(--sp-xs);
        }

        .log-note-why {
            margin: 0;
            padding: var(--sp-xs) var(--sp-lg) var(--sp-sm);
            font-family: var(--log-write);
            font-size: var(--fs-small);
            line-height: 1.45;
            color: var(--log-ink-soft);
            border-bottom: 1px solid var(--log-rule);
        }

        .log-note a {
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.14em;
            text-transform: uppercase;
            color: var(--log-magenta);
            text-decoration: underline;
            text-underline-offset: 0.25em;
            text-decoration-thickness: 1px;
            padding: var(--sp-2xs) 0;
            min-height: 44px;
            display: inline-flex;
            align-items: center;
        }

        /* ---- Actions --------------------------------------------------------- */

        /* The sheet's unused rows. A deck log is ruled to the foot of the page whether
           or not anything is written on it, so the space between the last entry and the
           primary action is ruled rather than blank — the 224px void the centred form
           used to leave is now the part of the instrument that has not been filled in
           yet. Row pitch matches a real entry row exactly, and the margin column and its
           rule carry straight down. Purely presentational, hence aria-hidden. */
        .log-fill {
            flex: 1 1 auto;
            /* One row is the floor, not zero: .log-status is positioned inside the first
               row, so on a short viewport a squeezed fill would clip the sign-in error
               entirely. The sheet scrolls instead. */
            min-height: var(--log-row);
            position: relative;
            border-bottom: 1px solid var(--log-rule-firm);
            background-image:
                repeating-linear-gradient(to bottom,
                    var(--log-rule) 0 1px,
                    transparent 1px var(--log-row));
            background-position: 0 -1px;
        }

        .log-fill::before {
            content: '';
            position: absolute;
            inset: 0 auto 0 0;
            width: 6.5rem;
            background: var(--log-stock-band);
            border-right: 1px solid var(--log-rule-firm);
        }

        /* The first unwritten row is where this attempt gets recorded. That is what
           makes the ruling structural rather than a picture of a log: the rows below
           the credentials are the space the sheet reserves for what happened, and a
           failed sign-in is written into the next line instead of floating past in a
           toast the traveller may already have looked away from. */
        /* The mark in the margin, beside the entry — a status written into the margin
           column is an entry in the log; red text floating on ruled paper is not. */
        .log-status::before {
            content: attr(data-mark);
            position: absolute;
            top: 0;
            left: -6.5rem;
            width: 6.5rem;
            height: 100%;
            display: flex;
            align-items: center;
            padding: 0 var(--sp-sm);
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.14em;
            text-transform: uppercase;
            color: inherit;
            line-height: 1.25;
        }

        .log-status {
            position: absolute;
            top: 0;
            left: 6.5rem;
            right: 0;
            min-height: var(--log-row);
            margin: 0;
            display: flex;
            align-items: center;
            padding: var(--sp-sm) var(--sp-md);
            font-family: var(--log-write);
            font-size: var(--fs-small);
            line-height: 1.35;
            color: var(--log-port);
        }

        .log-status:empty {
            display: none;
        }

        /* A caution is the chart's annotation colour; a refusal is port. */
        .log-status[data-kind="warning"] {
            color: var(--log-magenta);
        }

        .log-actions {
            padding: var(--sp-lg) var(--sp-lg) calc(var(--sp-lg) + env(safe-area-inset-bottom, 0px));
        }

        /* ENTER repeats the cover band: same oilskin, full width, no radius. */
        .log-stamp {
            display: block;
            width: 100%;
            background: var(--log-oilskin);
            color: var(--log-on-oilskin);
            border: none;
            font-family: var(--log-print);
            font-size: var(--fs-base);
            font-weight: 700;
            letter-spacing: 0.22em;
            text-transform: uppercase;
            min-height: 52px;
            padding: var(--sp-md);
            cursor: pointer;
        }

        .log-stamp:active {
            background: #000;
        }

        .log-stamp[disabled] {
            background: var(--log-rule-firm);
            color: var(--log-stock);
            cursor: default;
        }

        .log-alt {
            display: flex;
            align-items: center;
            gap: var(--sp-md);
            margin: var(--sp-lg) 0 var(--sp-md);
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.18em;
            text-transform: uppercase;
            color: var(--log-ink);
        }

        .log-alt::before,
        .log-alt::after {
            content: '';
            flex: 1;
            height: 1px;
            background: var(--log-rule-firm);
        }

        /* Google renders its own button and mandates its branding, so only the frame
           is ours. A hairline in the sheet's own firm rule keeps it inside the ruling
           instead of floating on it as a foreign dark slab. */
        .log-google {
            border-top: 1px solid var(--log-rule-firm);
            border-bottom: 1px solid var(--log-rule-firm);
            display: grid;
            grid-template-columns: 6.5rem 1fr;
            align-items: center;
        }

        .log-google > .log-entry-label {
            border-right: 1px solid var(--log-rule-firm);
            align-self: stretch;
        }

        .google-signin-wrap {
            width: 100%;
            min-height: 44px;
            padding: var(--sp-xs);
        }

        /* Google's button has a floor of about 200px and will overhang rather than
           shrink. Beside a 6.5rem margin column the entry cell falls under that floor
           below 375px, so the row stacks: the label becomes a ruled head over the
           button instead of a column beside it, and the closing rule still holds. */
        @media (max-width: 374px) {
            .log-google {
                grid-template-columns: 1fr;
            }

            .log-google > .log-entry-label {
                border-right: none;
                border-bottom: 1px solid var(--log-rule);
            }
        }

        .google-signin-wrap > div,
        .google-signin-wrap > div > div,
        .google-signin-wrap [role="button"] {
            width: 100%;
            max-width: none;
        }

        /* The registration action is separated by a rule, not by a gap: it is a
           different errand from signing in, and the sheet says so with a line. */
        .log-open {
            margin-top: var(--sp-lg);
            border-top: 1px solid var(--log-rule-firm);
            padding-top: var(--sp-lg);
        }

        .log-open-btn {
            display: block;
            width: 100%;
            background: transparent;
            color: var(--log-ink);
            border: 1px solid var(--log-ink);
            font-family: var(--log-print);
            font-size: var(--fs-body);
            font-weight: 600;
            letter-spacing: 0.18em;
            text-transform: uppercase;
            min-height: 48px;
            padding: var(--sp-sm) var(--sp-md);
            cursor: pointer;
        }

        .log-open-btn:active {
            background: var(--log-ink);
            color: var(--log-stock);
        }

        /* ---- Register sheet -------------------------------------------------- */

        .log-back {
            background: transparent;
            border: none;
            color: var(--log-on-oilskin);
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.16em;
            text-transform: uppercase;
            /* A word, underlined, as every way out in the log is (2026-09-14: the arrow beside it went). */
            text-decoration: underline;
            text-underline-offset: 0.25em;
            text-decoration-thickness: 1px;
            display: inline-flex;
            align-items: center;
            min-height: 44px;
            padding: 0;
            margin-bottom: var(--sp-xs);
            cursor: pointer;
        }

        .log-legend {
            font-family: var(--log-print);
            font-size: var(--fs-micro);
            font-weight: 600;
            letter-spacing: 0.16em;
            text-transform: uppercase;
            color: var(--log-ink);
            background: var(--log-stock-band);
            border-bottom: 1px solid var(--log-rule-firm);
            padding: var(--sp-sm) var(--sp-lg);
        }

        .log-terms {
            padding: var(--sp-md) var(--sp-lg);
            border-bottom: 1px solid var(--log-rule);
        }

        .log-terms label {
            display: flex;
            align-items: flex-start;
            gap: var(--sp-sm);
            font-family: var(--log-write);
            font-size: var(--fs-small);
            color: var(--log-ink);
            line-height: 1.5;
            cursor: pointer;
            min-height: 44px;
        }

        .log-terms input[type="checkbox"] {
            appearance: none;
            -webkit-appearance: none;
            width: 20px;
            height: 20px;
            flex: none;
            margin-top: 1px;
            border: 1px solid var(--log-rule-firm);
            background: var(--log-stock);
            cursor: pointer;
            display: grid;
            place-content: center;
        }

        /* Checked is a struck box, drawn in ink — the same correction mark the reveal
           control uses, so the sheet has one gesture for "done", not two.
           ::before, not ::after: the app-wide tap-target rule already owns
           #agreeTerms::after as a 44x44 invisible hit area, and painting a background
           into it turned the checkbox into a 44px violet slab sitting over the label. */
        .log-terms input[type="checkbox"]::before {
            content: '';
            width: 12px;
            height: 2px;
            background: var(--log-ink);
            transform: rotate(-45deg) scaleX(0);
            transform-origin: center;
            transition: transform 140ms cubic-bezier(0.2, 0.7, 0.3, 1);
        }

        .log-terms input[type="checkbox"]:checked {
            border-color: var(--log-ink);
        }

        .log-terms input[type="checkbox"]:checked::before {
            transform: rotate(-45deg) scaleX(1);
        }

        /* The one ink, not the magenta.
           Magenta on this sheet means an act that changes the record -- Enter, and Create
           account. Opening a document to read it changes nothing, and two links painted the
           same colour as the button that makes an account told a reader they were the same
           kind of thing. Flagged by the About finish review, 2026-09-08, which asked for a
           ruling on it; this is the ruling. */
        .log-terms a {
            color: var(--log-ink);
            text-decoration: underline;
            text-underline-offset: 0.2em;
        }

        .log-prompt {
            margin-top: var(--sp-lg);
            text-align: center;
            font-family: var(--log-write);
            font-size: var(--fs-small);
            color: var(--log-ink);
        }

        /* The one ink. Moving between the two sheets changes no record, and the
           magenta on this word made it look like the act rather than the way back
           to the other form. Same ruling as .log-terms a, 2026-09-08. */
        .log-prompt a {
            color: var(--log-ink);
            font-weight: var(--fw-medium);
            text-decoration: underline;
            text-underline-offset: 0.2em;
        }

        /* The document ground behind the sheet. #D9DFDD mirrors --log-stock, written as
           a literal because these selectors sit outside .login-page and so cannot see the
           scoped token. Below 768px this is what an overscroll bounce exposes on the
           Capacitor build; at desktop widths body stays #242424 (the frame's surround)
           and only the frame itself is repainted. */
        .app-frame:has(> .page.login-page.active),
        .app-frame:has(> .page.register-page.active) {
            background: #D9DFDD;
        }

        @media (max-width: 767px) {

            body:has(.page.login-page.active),
            body:has(.page.register-page.active) {
                background: #D9DFDD;
            }
        }

        /* ---- Browser surfaces this world also owns ---------------------------- */

        .login-page ::selection,
        .register-page ::selection {
            background: var(--log-magenta);
            color: #FFF;
        }

        .login-page :focus-visible,
        .register-page :focus-visible {
            outline: 2px solid var(--log-ink);
            outline-offset: 2px;
        }
        /* On the oilskin band, ink is 1.5:1: the band's own light ink instead (focus audit, 2026-09-14). */
        .log-band :focus-visible {
            outline-color: var(--log-on-oilskin);
        }
        /* ⚠️ A FIELD'S FOCUS IS ITS ROW'S BASELINE (finish review of the focus round, 2026-09-14). The page's outline above and
           the global field rule's 2px border both reached the value cell, so EMAIL drew a box, a border and the row's own
           thickened baseline at once. Neither is drawn now; the outline is transparent rather than none, so Windows High
           Contrast, which removes the row's box-shadow, still draws one. */
        .login-page .log-entry-value:focus-visible,
        .register-page .log-entry-value:focus-visible {
            outline: 2px solid transparent;
            outline-offset: -2px;
            border-bottom: none;
        }

        /* ---- The one authored moment ------------------------------------------ */
        /* The sheet is already fully visible on arrival; the only thing that moves is
           the margin rule being drawn down the column, once. Nothing else on this
           screen animates, including on return visits within the session. */

        @keyframes log-rule-draw {
            from { transform: scaleY(0); }
            to { transform: scaleY(1); }
        }

        .log-entries::before {
            content: '';
            position: absolute;
            inset: 0 auto 0 0;
            width: 0;
        }

        .log-entries {
            position: relative;
        }

        .login-page.active .log-entry-label,
        .register-page.active .log-entry-label {
            /* The border-right is the rule being drawn; animating a pseudo-element
               overlay rather than the border keeps subpixel rendering identical
               before and after. */
            position: relative;
        }

        .login-page.active .log-entries .log-entry:first-child .log-entry-label::after {
            content: '';
            position: absolute;
            top: 0;
            right: -1px;
            width: 1px;
            height: 100%;
            background: var(--log-ink);
            transform-origin: top;
            animation: log-rule-draw 420ms cubic-bezier(0.16, 1, 0.3, 1) both;
        }

        @media (prefers-reduced-motion: reduce) {
            .login-page.active .log-entries .log-entry:first-child .log-entry-label::after,
            .log-terms input[type="checkbox"]::before {
                animation: none;
                transition: none;
            }
        }

        /* ---- Narrow phones ---------------------------------------------------- */

        @media (max-width: 360px) {
            .log-entry {
                grid-template-columns: 5.25rem 1fr auto;
            }

            .log-band {
                padding-left: var(--sp-md);
                padding-right: var(--sp-md);
            }

            .log-actions,
            .log-note,
            .log-legend,
            .log-terms {
                padding-left: var(--sp-md);
                padding-right: var(--sp-md);
            }
        }

        /* Header */
