
        /* Registers --gp-modal-shift-x as an actual <length> instead of
           a generic, opaque custom property. Without this, a change to
           this variable's value (a different pin needing a different
           amount of edge-correction - see fitDockedModal() in
           globe-init.js) is NOT something the browser can smoothly
           interpolate, even though transform itself has a transition -
           unregistered custom properties are treated as discrete text
           substitutions, so the modal SNAPPED straight to its new
           position on every Next/Prev instead of sliding, which is what
           actually read as it jumping around. */
        @property --gp-modal-shift-x {
            syntax: '<length>';
            inherits: true;
            initial-value: 0px;
        }
        /* Same reasoning as --gp-modal-shift-x above, for the modal's
           width cap (see fitDockedModal() in globe-init.js) - without
           this, shrinking the modal to fit a tighter layout would also
           snap instead of resizing smoothly. */
        @property --gp-modal-max-w {
            syntax: '<length>';
            inherits: true;
            initial-value: 640px;
        }
        .granpai-globe-wrapper {
            position: relative;
            width: 100%;
            /* height:100% (forced with !important - see below) fills a
               parent that already has an explicit height (a hero
               section, a page-builder column with a fixed height, etc).
               aspect-ratio:1 is the actual sizing mechanism for every
               other case (parent gives no explicit height, or a height
               that's inconsistent across breakpoints) - it derives a
               real, always-responsive height directly from whatever
               width the wrapper ends up with at any given breakpoint,
               instead of a fixed pixel fallback that would stay locked
               once set and crop the globe on narrow widths. !important
               is needed because the JS near-zero-height fallback in
               globe-init.js sets an inline height style directly on
               this element in some edge cases, and inline styles
               otherwise beat this rule. */
            height: 100% !important;
            aspect-ratio: 1;
            /* Safety floor: even with the JS-side fix (waiting for
               document.fonts.ready before the globe's first real
               resize), a genuinely pathological page could still hand
               the globe an initial container size well below its final
               one for a moment. A reasonable minimum keeps that moment,
               if it ever happens, from looking like a cramped single
               dot-cluster instead of a recognizable globe. */
            min-width: 220px;
            min-height: 220px;
            /* Transparent by default so the map sits cleanly on top of
               any section background (image, gradient, solid color)
               instead of punching an opaque box into the page. The globe
               canvas itself already renders with alpha:true + a
               transparent backgroundColor (see globe-init.js) for the
               Light and Minimal styles; Photoreal Dark intentionally
               keeps its own starfield backdrop image for that specific
               look. Give a wrapper its own backdrop by overriding
               --gp-wrapper-bg (or --gp-bg to also affect the flat map),
               e.g. inline: style='--gp-wrapper-bg:#03030a'. */
            background-color: var(--gp-wrapper-bg, transparent);
            container-type: inline-size;
            container-name: granpai-globe;
            /* Column flex: on narrow/mobile screens the modal (2nd child,
               see below) simply stacks below the map in normal flow. On
               wider screens the modal switches to position:absolute (see
               the docked override further down) and escapes this layout
               entirely, so flex-direction stops mattering there - the
               stage-clip ends up filling the whole wrapper either way. */
            display: flex;
            flex-direction: column;
            /* Auto-Balance Layout (Pro) - no rule here ever sets a
               transform on this element by default, so this transition
               is a harmless no-op unless that feature is both enabled
               and active, at which point pro-frontend.js sets
               transform:translateX(...) via inline style when a pin's
               modal (or Pin List Sidebar) opens - this is what makes
               that shift slide smoothly instead of jumping straight to
               its new position. Timing roughly matches the modal's own
               opacity/transform transition (see .granpai-modal-overlay
               further down) so both move as one connected motion.
               */
            transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
        }

        /* Holds the actual canvas/SVG and clips it - kept separate from
           the wrapper itself (which is NOT overflow:hidden) so the
           desktop-docked modal can be positioned outside the wrapper's
           box (left/right, per data-modal-side) without being clipped
           back off by the same overflow rule that keeps the globe
           canvas tidy. */
        .granpai-stage-clip {
            position: relative;
            width: 100%;
            flex: 1 1 auto;
            min-height: 0;
            overflow: hidden;
            border-radius: var(--gp-radius, 10px);
        }
        /* Loading state: a soft pulsing circle standing in for the globe
           about to appear, rather than a generic shimmer rectangle -
           two concentric rings breathing slightly out of phase for a
           layered feel instead of one flat pulse. Pure CSS, no JS
           needed to run it - only to remove it (see --ready below) once
           there's something real to show instead. */
        .granpai-stage-clip::before,
        .granpai-stage-clip::after {
            content: '';
            position: absolute;
            top: 50%;
            left: 50%;
            border-radius: 50%;
            border: 1px solid rgba(255, 255, 255, 0.18);
            transform: translate(-50%, -50%) scale(0.85);
            opacity: 0;
        }
        .granpai-stage-clip::before {
            width: min(34%, 200px);
            aspect-ratio: 1;
            background: radial-gradient(circle, rgba(255, 255, 255, 0.10), rgba(255, 255, 255, 0.01) 72%);
            animation: granpai-globe-pulse 2.2s ease-in-out infinite;
        }
        .granpai-stage-clip::after {
            width: min(50%, 300px);
            aspect-ratio: 1;
            border-color: rgba(255, 255, 255, 0.08);
            animation: granpai-globe-pulse 2.2s ease-in-out infinite 0.5s;
        }
        /* Added by JS (see the requestAnimationFrame/draw() calls in
           globe-init.js) the moment the globe/flat map has actually
           rendered - stops both rings so they don't linger behind the
           real canvas/SVG content once there's something to show. */
        .granpai-stage-clip--ready::before,
        .granpai-stage-clip--ready::after {
            display: none;
        }
        @keyframes granpai-globe-pulse {
            0%   { transform: translate(-50%, -50%) scale(0.85); opacity: 0; }
            35%  { opacity: 1; }
            100% { transform: translate(-50%, -50%) scale(1.15); opacity: 0; }
        }
        /* Toggled by JS on marker mouseenter/mouseleave (see
           buildPulseMarkerEl in globe-init.js) - lets the hover-preview
           tag (.granpai-pin-tag, further down) pop out past this
           container's own rounded-corner clip instead of being cut off,
           which it otherwise would be whenever a pin sits close enough
           to the globe's own edge. Reverted the instant hover ends, so
           the canvas/SVG itself stays properly clipped to its rounded
           box the rest of the time - this isn't a permanent change to
           the container, just a brief exception for the moment a tag
           actually needs the room. */
        .granpai-stage-clip--hover-overflow {
            overflow: visible;
        }
        /* Applied by JS (see openPinModal/closePinModal in globe-init.js)
           to whatever sits OUTSIDE the map/modal's own branch of the DOM,
           within the nearest section-like ancestor - draws focus toward
           the open pin modal without touching the map/modal themselves. */
        .granpai-blur-sibling {
            filter: blur(6px);
            transition: filter 0.3s ease;
            pointer-events: none;
        }

        /* Plain class selectors (not scoped to a specific wrapper ID) so
           these custom properties also resolve on the modal overlay,
           which is a sibling of the wrapper, not a descendant. */
        .granpai-theme-dark {
            --gp-bg: #03030a;
            --gp-surface: #0a0c18;
            --gp-accent: #00ffcc;
            --gp-accent-soft: rgba(0, 255, 204, 0.35);
            --gp-text: #ffffff;
            --gp-text-muted: #d1d1de;
            --gp-grid-line: rgba(255, 255, 255, 0.06);
            --gp-land-fill: rgba(0, 255, 204, 0.10);
            --gp-land-stroke: rgba(0, 255, 204, 0.55);
        }
        .granpai-theme-light {
            --gp-bg: transparent;
            --gp-surface: #ffffff;
            --gp-accent: #0d9488;
            --gp-accent-soft: rgba(13, 148, 136, 0.35);
            --gp-text: #14151f;
            --gp-text-muted: #565971;
            --gp-grid-line: rgba(20, 21, 31, 0.08);
            --gp-land-fill: rgba(13, 148, 136, 0.14);
            --gp-land-stroke: rgba(13, 148, 136, 0.65);
        }

        .granpai-globe-wrapper [data-role='render-target'] {
            width: 100%;
            height: 100%;
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .granpai-modal-overlay {
            position: static;
            width: 100%;
            flex: 0 0 auto;
            /* CSS Grid 0fr/1fr technique: animates to the content's
               TRUE height instead of a fixed max-height target (the
               previous max-height:0->60vh approach spent most of its
               transition time not visually changing anything, since
               real content is usually shorter than 60vh, then snapped
               shut abruptly right at the end when closing - felt like
               a jarring bounce instead of a smooth close). */
            display: grid;
            grid-template-rows: 0fr;
            background: transparent;
            opacity: 0;
            visibility: visible;
            transition: grid-template-rows 0.38s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.28s ease;
        }
        .granpai-modal-overlay.is-open {
            grid-template-rows: 1fr;
            opacity: 1;
        }
        .granpai-modal-box {
            position: relative;
            display: flex;
            flex-direction: column;
            background: var(--gp-surface, #0a0c18);
            border: 1px solid var(--gp-modal-border-color, var(--gp-accent, #00ffcc));
            border-radius: 10px;
            width: 100%;
            max-width: none;
            max-height: 60vh;
            min-height: 0;
            overflow: hidden;
            padding: 22px 20px 20px;
            color: var(--gp-text, #ffffff);
            box-shadow: 0 -8px 24px rgba(0,0,0,0.25);
        }
        .granpai-modal-scroll {
            flex: 1 1 auto;
            min-height: 0;
            overflow-y: auto;
            overflow-x: hidden;
            /* Firefox */
            scrollbar-width: thin;
            scrollbar-color: var(--gp-accent, #00ffcc) transparent;
        }
        .granpai-modal-scroll::-webkit-scrollbar {
            width: 6px;
        }
        .granpai-modal-scroll::-webkit-scrollbar-track {
            background: transparent;
        }
        .granpai-modal-scroll::-webkit-scrollbar-thumb {
            background: var(--gp-accent, #00ffcc);
            border-radius: 3px;
        }
        .granpai-modal-footer {
            flex: 0 0 auto;
        }
        .granpai-modal-close {
            position: absolute;
            top: 12px;
            right: 14px;
            width: 30px;
            height: 30px;
            display: flex;
            align-items: center;
            justify-content: center;
            /* Fixed dark, semi-opaque backdrop + white icon regardless of
               theme or what's underneath (a pin image especially - a
               light/translucent circle disappeared against light photos;
               a dark circle with a white glyph stays readable against
               any image or background color). */
            background: rgba(0, 0, 0, 0.55);
            border: 1px solid rgba(255, 255, 255, 0.25);
            border-radius: 50%;
            box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
            color: #fff;
            font-size: 20px;
            line-height: 1;
            cursor: pointer;
            padding: 4px;
            z-index: 2;
        }
        .granpai-modal-close:hover {
            background: rgba(0, 0, 0, 0.8);
        }
        .granpai-modal-tag {
            color: var(--gp-modal-tag-color, var(--gp-accent));
            text-transform: uppercase;
            font-size: 11px;
            font-weight: 800;
            letter-spacing: 2px;
            display: block;
            margin-bottom: 10px;
        }
        .granpai-modal-title {
            margin: 0 0 14px 0;
            font-size: 20px;
            line-height: 1.35;
            font-weight: 700;
            border-bottom: 1px solid var(--gp-grid-line);
            padding-bottom: 12px;
        }
        .granpai-modal-body {
            margin: 0;
            font-size: 14px;
            line-height: 1.7;
            color: var(--gp-text-muted);
            white-space: pre-line;
        }
        .granpai-modal-media {
            margin: 0 0 16px;
        }
        .granpai-modal-media img {
            display: block;
            width: 100%;
            height: auto;
            border-radius: 10px 10px 0 0;
        }
        /* Logo mode (Pro Pin/modal image setting = Logo) - small,
           contained square instead of the full-width banner Photo mode
           uses. Pulled out of normal document flow and placed next to
           the title instead of above it, since a small logo sitting
           alone at the top like a banner would look like an accidental
           gap, not a deliberate compact mark. */
        .granpai-modal-media--logo {
            float: left;
            width: 56px;
            height: 56px;
            margin: 0 14px 10px 0;
        }
        .granpai-modal-media--logo img {
            width: 56px;
            height: 56px;
            object-fit: contain;
            border-radius: 8px;
            background: var(--gp-grid-line);
            padding: 6px;
            box-sizing: border-box;
        }
        /* Only the tag+title sit beside the floated logo - body text
           clears below it instead of also wrapping around, which would
           look cramped for anything longer than a couple of words. */
        .granpai-modal-media--logo ~ .granpai-modal-body {
            clear: both;
        }
        .granpai-modal-contacts {
            margin-top: 16px;
            padding-top: 16px;
            border-top: 1px solid var(--gp-grid-line);
        }
        /* email / phone - readable rows, icon + actual value */
        .granpai-modal-contacts__info {
            display: flex;
            flex-direction: column;
            gap: 10px;
        }
        .granpai-modal-contacts__info a {
            display: inline-flex;
            align-items: center;
            gap: 12px;
            color: var(--gp-text, #fff);
            text-decoration: none;
            font-size: 0.92rem;
            transition: color 0.15s ease;
        }
        .granpai-modal-contacts__info a:hover {
            color: var(--gp-modal-tag-color, var(--gp-accent));
        }
        .granpai-modal-contacts__info svg {
            width: 17px;
            height: 17px;
            fill: currentColor;
            flex-shrink: 0;
            opacity: 0.75;
        }
        .granpai-modal-contacts__info a:hover svg { opacity: 1; }
        .granpai-modal-contacts__label {
            overflow-wrap: anywhere;
        }
        /* website / social - original compact icon-only row */
        .granpai-modal-contacts__social {
            display: flex;
            flex-wrap: wrap;
            gap: 10px;
        }
        .granpai-modal-contacts__info + .granpai-modal-contacts__social {
            margin-top: 14px;
        }
        .granpai-modal-contacts__social a {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            width: 32px;
            height: 32px;
            border-radius: 50%;
            background: var(--gp-grid-line);
            border: 1px solid var(--gp-grid-line);
            box-sizing: border-box;
            color: var(--gp-text, #fff);
            text-decoration: none;
            transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
        }
        .granpai-modal-contacts__social a:hover {
            background: var(--gp-modal-tag-color, var(--gp-accent));
            border-color: var(--gp-modal-tag-color, var(--gp-accent));
            color: #ffffff;
        }
        .granpai-modal-contacts__social svg {
            width: 15px;
            height: 15px;
            fill: currentColor;
        }
        .granpai-modal-nav {
            display: flex;
            flex-wrap: wrap;
            align-items: center;
            justify-content: flex-start;
            gap: 10px;
            row-gap: 8px;
            margin-top: 16px;
            padding-top: 16px;
            border-top: 1px solid var(--gp-grid-line);
        }
        /* Pushes Prev (and, right after it, Next) all the way to the
           right edge - Get Directions (inserted before Prev, see
           pro-frontend.js) is left wherever it naturally sits at the
           start of the row, so the two actions read as clearly separate
           groups instead of three buttons evenly spaced across the row. */
        .granpai-modal-nav-prev {
            margin-left: auto;
        }
        .granpai-modal-nav-btn {
            background: transparent;
            border: 1px solid var(--gp-grid-line);
            color: var(--gp-text, #fff);
            border-radius: 6px;
            padding: 7px 14px;
            font-size: 13px;
            cursor: pointer;
            transition: border-color 0.15s ease, color 0.15s ease;
        }
        .granpai-modal-nav-btn:hover {
            border-color: var(--gp-accent);
            color: var(--gp-accent);
        }

        /* ---------- Mobile: fixed 50/50 split (globe always visible, modal as a bottom sheet) ----------
           Below the desktop-docked breakpoint (see the two blocks after
           this one), the globe/map wrapper is pinned to exactly half
           the viewport height and stays there whether or not a pin's
           modal is open - previously the wrapper's height came purely
           from aspect-ratio against its own width, which on a narrow
           phone screen could render smaller than the space actually
           available, and opening a modal pushed the globe out of view
           entirely (normal document flow, modal expanding inline above
           it). Now the modal is a fixed bottom sheet instead, sliding up
           over the bottom half while the globe keeps rendering/rotating
           in the top half the whole time - matches how the desktop
           docked panel already keeps the map interactive alongside an
           open modal, just stacked instead of side-by-side. */
        @media (max-width: 640px) {
            /* Actual available height is (100vh - sticky header), not
               100vh outright - see the modal-open rule further down for
               why. Default (no modal open) gives the globe a roomy 85%
               of that instead of splitting evenly with a modal that
               isn't even visible yet - it only needs to give up room
               once a pin's modal is actually open, not permanently sized
               for that case. */
            .granpai-globe-wrapper {
                height: calc((100vh - 76px) * 0.65) !important;
                aspect-ratio: unset;
                transition: height 0.3s ease;
                /* Matches the site's sticky header height - without
                   this, scrollIntoView() (see openPinModal() in
                   globe-init.js, triggered when a pin is tapped) would
                   align the wrapper's top edge exactly with the
                   viewport's top, which is where the sticky header
                   sits, tucking the globe's own top portion behind it. */
                scroll-margin-top: 76px;
            }
            /* Shrinks to the 50/50 split only while a pin's modal is
               actually open (:has() reacts live to the .is-open class
               JS toggles on the modal below) - and grows back to the
               roomy 85% default the instant it closes, rather than
               staying shrunk permanently. The previous 50vh+38px /
               50vh-38px split (before this) summed to exactly 100vh on
               its own, which never left room for the header sitting on
               top of that same viewport space, so wrapper+modal
               together always overflowed by the header's own height
               regardless of Timeline - this and the timeline-bar rule
               below are both computed from (100vh - header) instead. */
            .granpai-globe-wrapper:has(.granpai-modal-overlay.is-open) {
                height: calc((100vh - 76px) * 0.5) !important;
            }
            .granpai-modal-overlay {
                position: fixed;
                left: 0;
                right: 0;
                bottom: 0;
                width: 100%;
                height: calc((100vh - 76px) * 0.5);
                display: block;
                grid-template-rows: unset;
                background: transparent;
                opacity: 0;
                visibility: hidden;
                pointer-events: none;
                transform: translateY(16px);
                transition: opacity 0.3s ease, transform 0.3s ease, visibility 0s linear 0.3s;
                /* Above the parent theme's back-to-top button
                   (z-index 999980) so an open modal is never partially
                   covered by it, and below GranpaiEdit's own editing
                   fab (999990) so that stays reachable while editing. */
                z-index: 999985;
            }
            .granpai-modal-overlay.is-open {
                opacity: 1;
                visibility: visible;
                pointer-events: auto;
                transform: translateY(0);
                transition: opacity 0.3s ease, transform 0.3s ease;
            }
            .granpai-modal-box {
                height: 100%;
                max-height: 100%;
                border-radius: 16px 16px 0 0;
                box-shadow: 0 -8px 32px rgba(0,0,0,0.35);
            }
            .granpai-timeline-bar {
                padding: 8px 12px;
                gap: 10px;
            }
            .granpai-timeline-play {
                width: 30px;
                height: 30px;
            }
            .granpai-timeline-nodes {
                height: 40px;
                gap: 26px;
            }
            /* Timeline bar's real height on mobile is ~64px (40px nodes
               + 16px vertical padding + 8px margin-top, from the rules
               just above) - added on top of whichever wrapper state is
               currently active (roomy default or modal-open-shrunk),
               only when a visible timeline bar actually exists as a
               sibling, so wrapper+modal still sum to the same (100vh -
               header) total either way instead of the timeline bar
               eating into space neither element accounted for. */
            .granpai-globe-wrapper:has(.granpai-timeline-bar:not([hidden])) {
                height: calc((100vh - 76px) * 0.65 + 64px) !important;
            }
            .granpai-globe-wrapper:has(.granpai-timeline-bar:not([hidden])):has(.granpai-modal-overlay.is-open) {
                height: calc((100vh - 76px) * 0.5 + 64px) !important;
            }
            .granpai-globe-wrapper:has(.granpai-timeline-bar:not([hidden])) .granpai-modal-overlay {
                height: calc((100vh - 76px) * 0.5 - 64px);
            }
        }

        /* ---------- Responsive modal: overlay (mobile) vs docked panel (wider maps) ----------
           Base rule above (.granpai-modal-overlay / .granpai-modal-box)
           is the full-screen, must-close-to-continue overlay - correct
           for small screens. The two @supports blocks below layer a
           docked, non-blocking side-panel treatment on top of it once
           the map itself is wide enough: clicking a different pin then
           just swaps the panel's content in place, and the rest of the
           map stays clickable underneath.

           The two blocks are mutually exclusive (container-type support
           vs not), so exactly one applies in any given browser - no
           override war between them. Preferred: a CSS container query,
           keyed to THIS map's own rendered width rather than the whole
           viewport, so a map embedded in a narrow sidebar still correctly
           gets the mobile-style overlay even on a wide desktop screen.
           Fallback: a plain viewport media query for browsers without
           container query support. */
        @supports (container-type: inline-size) {
            @container granpai-globe (min-width: 520px) {
                .granpai-modal-overlay {
                    position: absolute;
                    top: 50%;
                    /* Closed state: slightly scaled down + fully
                       transparent for a soft pop-in entrance instead
                       of appearing instantly - .is-open below animates
                       both opacity and this transform together. */
                    transform: translateY(calc(-50% - 10px)) scale(0.94);
                    left: 100%;
                    right: auto;
                    margin-left: 16px;
                    /* var(--gp-modal-max-w) - set via JS
                       (fitDockedModal() in globe-init.js) - caps width
                       further when there genuinely isn't 640px/65%
                       worth of room to the side, e.g. a narrower page
                       layout (a control panel or sidebar eating into
                       the available width). Shifting the modal back
                       into view (see --gp-modal-shift-x) only solves a
                       WRONG position; if the modal is simply too WIDE
                       for the space next to it, no amount of shifting
                       can make it fit without also overlapping its own
                       trigger (the map) - shrinking it is the other
                       half of the fix. Defaults to the original
                       min(640px, 65%) when JS hasn't set anything. */
                    width: min(640px, 65%, var(--gp-modal-max-w, 640px));
                    /* Viewport-relative, NOT tied to the globe container's
                       own height - a square/short hero embed shouldn't
                       cap how tall the modal can be on a roomy laptop
                       screen. This is a definite height (needed so the
                       box's height:100% below has something real to
                       resolve against) rather than 100% of the wrapper. */
                    /* Content-driven height, capped rather than
                       forced - short pin descriptions used to be
                       stretched into a fixed 85vh/700px box with a lot
                       of dead space at the bottom. Long content still
                       caps here and scrolls internally (see
                       .granpai-modal-scroll). */
                    height: auto;
                    max-height: min(85vh, 700px);
                    overflow: visible;
                    background: transparent;
                    opacity: 0;
                    pointer-events: none;
                    transition: opacity 0.28s ease, transform 0.38s cubic-bezier(0.16, 1, 0.3, 1), width 0.3s ease;
                    z-index: var(--gp-modal-z-index, 999999);
                }
                .granpai-modal-overlay.is-open {
                    opacity: 1;
                    pointer-events: auto;
                    /* --gp-modal-shift-x: set via JS (fitDockedModal()
                       in globe-init.js) using setProperty, not by writing
                       to style.transform directly - that used to WIPE OUT
                       this translateY/scale entirely whenever a shift was
                       needed (inline styles always beat class rules), so
                       the modal snapped to the top instead of staying
                       centered every time an overflow correction fired.
                       Reading it as a variable here means JS can never
                       clobber the rest of this transform, no matter what. */
                    transform: translateY(-50%) scale(1) translateX(var(--gp-modal-shift-x, 0px));
                    transition: opacity 0.32s ease, transform 0.42s cubic-bezier(0.16, 1, 0.3, 1), width 0.3s ease;
                }
                .granpai-globe-wrapper[data-modal-side='left'] .granpai-modal-overlay {
                    left: auto;
                    right: 100%;
                    margin-left: 0;
                    margin-right: 16px;
                }
                .granpai-modal-box {
                    width: 100%;
                    height: auto;
                    max-height: 100%;
                    box-shadow: 0 20px 60px rgba(0,0,0,0.35);
                }
            }
        }
        @supports not (container-type: inline-size) {
            @media (min-width: 520px) {
                .granpai-modal-overlay {
                    position: absolute;
                    top: 50%;
                    /* Closed state: slightly scaled down + fully
                       transparent for a soft pop-in entrance instead
                       of appearing instantly - .is-open below animates
                       both opacity and this transform together. */
                    transform: translateY(calc(-50% - 10px)) scale(0.94);
                    left: 100%;
                    right: auto;
                    margin-left: 16px;
                    /* var(--gp-modal-max-w) - set via JS
                       (fitDockedModal() in globe-init.js) - caps width
                       further when there genuinely isn't 640px/65%
                       worth of room to the side, e.g. a narrower page
                       layout (a control panel or sidebar eating into
                       the available width). Shifting the modal back
                       into view (see --gp-modal-shift-x) only solves a
                       WRONG position; if the modal is simply too WIDE
                       for the space next to it, no amount of shifting
                       can make it fit without also overlapping its own
                       trigger (the map) - shrinking it is the other
                       half of the fix. Defaults to the original
                       min(640px, 65%) when JS hasn't set anything. */
                    width: min(640px, 65%, var(--gp-modal-max-w, 640px));
                    /* Viewport-relative, NOT tied to the globe container's
                       own height - a square/short hero embed shouldn't
                       cap how tall the modal can be on a roomy laptop
                       screen. This is a definite height (needed so the
                       box's height:100% below has something real to
                       resolve against) rather than 100% of the wrapper. */
                    /* Content-driven height, capped rather than
                       forced - short pin descriptions used to be
                       stretched into a fixed 85vh/700px box with a lot
                       of dead space at the bottom. Long content still
                       caps here and scrolls internally (see
                       .granpai-modal-scroll). */
                    height: auto;
                    max-height: min(85vh, 700px);
                    overflow: visible;
                    background: transparent;
                    opacity: 0;
                    pointer-events: none;
                    transition: opacity 0.28s ease, transform 0.38s cubic-bezier(0.16, 1, 0.3, 1), width 0.3s ease;
                    z-index: var(--gp-modal-z-index, 999999);
                }
                .granpai-modal-overlay.is-open {
                    opacity: 1;
                    pointer-events: auto;
                    /* --gp-modal-shift-x: set via JS (fitDockedModal()
                       in globe-init.js) using setProperty, not by writing
                       to style.transform directly - that used to WIPE OUT
                       this translateY/scale entirely whenever a shift was
                       needed (inline styles always beat class rules), so
                       the modal snapped to the top instead of staying
                       centered every time an overflow correction fired.
                       Reading it as a variable here means JS can never
                       clobber the rest of this transform, no matter what. */
                    transform: translateY(-50%) scale(1) translateX(var(--gp-modal-shift-x, 0px));
                    transition: opacity 0.32s ease, transform 0.42s cubic-bezier(0.16, 1, 0.3, 1), width 0.3s ease;
                }
                .granpai-globe-wrapper[data-modal-side='left'] .granpai-modal-overlay {
                    left: auto;
                    right: 100%;
                    margin-left: 0;
                    margin-right: 16px;
                }
                .granpai-modal-box {
                    width: 100%;
                    height: auto;
                    max-height: 100%;
                    box-shadow: 0 20px 60px rgba(0,0,0,0.35);
                }
            }
        }

        /* Tablet range (641px-1024px): overrides BOTH docked-beside
           blocks above with a plain fixed overlay pinned to one edge -
           no dynamic width-capping or shift-correction math at all. The
           dock-beside-the-globe, measure-overflow, nudge-back approach
           above kept oscillating on this specific range even after
           several rounds of fixing fitDockedModal() itself - there just
           isn't reliably enough room beside the globe at these widths for
           a beside treatment to make sense in the first place. An
           overlay avoids the problem instead of continuing to chase it:
           fixed size and position, nothing to recalculate, nothing to
           get wrong. Comes after both blocks above in source order so it
           wins regardless of which one the browser matched. */
        @media (min-width: 641px) and (max-width: 1024px) {
            .granpai-modal-overlay {
                position: absolute;
                top: 50%;
                bottom: auto;
                left: auto;
                right: 16px;
                margin: 0;
                width: min(380px, 90%);
                /* Content-driven, not forced to fill the wrapper - a
                   short pin description in a box stretched to the full
                   tablet height left a lot of dead space at the bottom,
                   which read as broken rather than intentional. */
                height: auto;
                max-height: min(75vh, 640px);
                overflow: visible;
                transform: translateY(-50%) translateX(16px) scale(0.96);
                display: block;
                grid-template-rows: unset;
                background: transparent;
                opacity: 0;
                visibility: hidden;
                pointer-events: none;
                transition: opacity 0.28s ease, transform 0.34s cubic-bezier(0.16, 1, 0.3, 1);
            }
            .granpai-modal-overlay.is-open {
                opacity: 1;
                visibility: visible;
                pointer-events: auto;
                transform: translateY(-50%) translateX(0) scale(1);
            }
            .granpai-globe-wrapper[data-modal-side='left'] .granpai-modal-overlay {
                right: auto;
                left: 16px;
                transform: translateY(-50%) translateX(-16px) scale(0.96);
            }
            .granpai-globe-wrapper[data-modal-side='left'] .granpai-modal-overlay.is-open {
                transform: translateY(-50%) translateX(0) scale(1);
            }
            .granpai-modal-box {
                width: 100%;
                height: auto;
                max-height: 100%;
                border-radius: 14px;
                box-shadow: 0 20px 60px rgba(0,0,0,0.35);
            }
            /* Leaves a bit more headroom above the Timeline bar - only
               applies when a visible timeline bar actually exists as a
               sibling, so maps without Timeline enabled keep the full
               75vh/640px cap (nothing below to avoid there). Adjusts the
               CAP rather than pinning a hard bottom offset, so the card
               stays a natural floating shape either way instead of
               getting clipped. */
            .granpai-globe-wrapper:has(.granpai-timeline-bar:not([hidden])) .granpai-modal-overlay {
                max-height: min(64vh, 560px);
            }
        }

        /* Docked-panel overflow (map embedded in a wide/full-width
           section, close enough to the section's own edge that the
           panel spills past the real browser viewport) is handled in
           JS - see fitDockedModal() in globe-init.js. It measures the
           exact overflow in pixels and nudges the panel back by only
           that much via transform: translateX(), rather than a fixed
           viewport-width guess or an all-or-nothing snap to the
           wrapper's edge. No CSS fallback is needed here: the map
           itself requires JS (WebGL/Canvas) to render at all. */

        .granpai-pulse-marker {
            position: relative;
            width: 24px;
            height: 24px;
            cursor: pointer;
            transform: translate(-12px, -12px);
            pointer-events: auto;
            z-index: 1;
        }
        /* z-index only resolves within a shared stacking context - the
           label's own z-index:3 (below) only wins against elements
           inside the SAME marker. When pins sit close together (fanned
           cluster), a neighboring marker's dot could still paint over
           this marker's label unless the whole marker itself is also
           raised on hover. !important is needed here specifically:
           globe.gl (three-globe/CSS2DRenderer) sets each marker's
           z-index via inline style every render frame based on camera
           depth, which otherwise always wins over a plain CSS rule. */
        .granpai-pulse-marker:hover {
            z-index: 10 !important;
        }
        /* Everything visual lives in here, so applyClusters() (see
           globe-init.js) can offset it with its own transform when
           fanning out a cluster, without touching the outer
           .granpai-pulse-marker - that's the element three-globe/Flat
           mode position at the pin's true anchor point, and re-set on
           every one of their own updates, which would otherwise
           overwrite anything added here directly on it. */
        .granpai-pulse-marker-inner {
            position: absolute;
            top: 0; left: 0;
            width: 24px; height: 24px;
            transition: transform 0.3s ease;
        }
        .granpai-pulse-dot {
            position: absolute;
            top: 6px; left: 6px;
            width: 12px; height: 12px;
            background: var(--gp-pin-color, var(--gp-accent));
            border-radius: 50%;
            box-shadow: 0 0 12px 3px var(--gp-pin-color-soft, var(--gp-accent-soft));
            z-index: 2;
        }
        .granpai-pulse-ring {
            position: absolute;
            top: 6px; left: 6px;
            width: 12px; height: 12px;
            border-radius: 50%;
            border: 2.5px solid var(--gp-pin-color, var(--gp-accent));
            animation: granpai-pulse-anim 2.2s ease-out infinite;
            z-index: 1;
        }
        .granpai-pulse-ring.granpai-pulse-delay {
            animation-delay: 1.1s;
        }
        @keyframes granpai-pulse-anim {
            0%   { transform: scale(1);   opacity: 0.75; }
            100% { transform: scale(4.5); opacity: 0; }
        }
        .granpai-pulse-marker--flash .granpai-pulse-dot {
            animation: granpai-pulse-flash 0.7s ease-out;
        }
        @keyframes granpai-pulse-flash {
            0%   { transform: scale(1);   box-shadow: 0 0 12px 3px var(--gp-pin-color-soft, var(--gp-accent-soft)); }
            35%  { transform: scale(2.1); box-shadow: 0 0 26px 12px var(--gp-pin-color-soft, var(--gp-accent-soft)); }
            100% { transform: scale(1);   box-shadow: 0 0 12px 3px var(--gp-pin-color-soft, var(--gp-accent-soft)); }
        }

        /* Two or more pins sharing (roughly) the same screen position
           (see recomputeClusters()/applyFanOut() in globe-init.js) are
           never hidden from each other - every one of them stays fully
           visible and independently clickable, each nudged out to its
           own spot in a small fan above the shared anchor and connected
           back to it with a thin leg line, drawn from that true anchor
           point (this element's own top:12px/left:12px - the center of
           the 24x24 marker box) out to wherever the JS-set width/rotate
           transform points it. */
        .granpai-pulse-marker-leg {
            position: absolute;
            top: 12px; left: 12px;
            height: 1px;
            width: 0;
            /* Dashed via a repeating gradient rather than
               border-style:dashed - a 1px dashed BORDER render
               inconsistently (sometimes invisible) across browsers at
               that thickness; a gradient background doesn't have that
               problem and still reads as a clean dashed guide line. */
            background-image: repeating-linear-gradient(
                to right,
                var(--gp-text-muted, rgba(234, 242, 255, 0.55)) 0,
                var(--gp-text-muted, rgba(234, 242, 255, 0.55)) 3px,
                transparent 3px,
                transparent 6px
            );
            opacity: 0;
            transform-origin: 0 50%;
            transition: width 0.3s ease, opacity 0.3s ease;
            pointer-events: none;
        }
        .granpai-pulse-marker--fanned .granpai-pulse-marker-leg {
            opacity: 0.4;
        }

        /* Timeline (Pro) - toggled in initTimelineControls() in
           globe-init.js based on each marker's own data-pin-date
           attribute (set in buildPulseMarkerEl). Fades rather than a
           hard display:none, so a pin doesn't just abruptly pop into
           existence the instant the slider passes its date. */
        .granpai-pulse-marker--timeline-hidden {
            opacity: 0;
            pointer-events: none;
            transition: opacity 0.4s ease;
        }


        /* Hover preview bubble - shows the pin's short 'location' label
           right above it, speech-bubble style. Pointer-events:none so it
           never steals the click/hover from the marker itself; visible
           purely via opacity+transform so it never affects layout/
           marker positioning math (that's all done in JS against the
           marker element's own untouched size). */
        .granpai-pin-tag {
            position: absolute;
            left: 50%;
            bottom: 100%;
            margin-bottom: 14px;
            transform: translate(-50%, 4px) scale(0.92);
            /* Was pointer-events:none - now interactive so hovering the
               tag itself (image/label), not just the tiny marker dot,
               keeps it visible. Needed because the camera-nudge-toward-
               edge-pin behavior (see buildPulseMarkerEl's mouseenter in
               globe-init.js) can slide the marker+tag away from
               wherever the cursor originally was - without this, the
               tag would vanish the instant the marker itself moved out
               from under the cursor, even if the tag's own body still
               happened to be sitting right there. Since the tag is a
               DOM child of the marker, hovering it also counts as
               hovering the marker for CSS :hover purposes (hover state
               bubbles up the ancestor chain), so the existing
               .granpai-pulse-marker:hover rule below just keeps working
               with no other changes needed. */
            pointer-events: auto;
            opacity: 0;
            z-index: 3;
            transition: opacity 0.18s ease, transform 0.18s ease;
            /* One unified shadow under the whole shape (body + tail),
               rather than a box-shadow that would only hug the body's
               rounded rectangle and leave the tail looking flat/pasted
               on. */
            filter: drop-shadow(0 4px 10px rgba(0, 0, 0, 0.3));
        }
        .granpai-pin-tag-img {
            display: block;
            width: 120px;
            height: 68px;
            object-fit: cover;
            border-radius: 8px 8px 0 0;
        }
        /* Logo mode (Pro Pin/modal image setting = Logo) - square and
           contained instead of a wide cropped photo, matching the text
           label's own 120px width but sized down since a logo doesn't
           need as much room as a location photo. */
        .granpai-pin-tag-img--logo {
            width: 120px;
            height: 60px;
            object-fit: contain;
            padding: 8px;
            box-sizing: border-box;
            background: var(--gp-grid-line);
        }
        .granpai-pin-tag-text {
            display: block;
            width: 120px;
            box-sizing: border-box;
            padding: 6px 10px;
            border-radius: 8px;
            position: relative;
            /* Blends in some dark regardless of how bright the site's pin
               color is - same reasoning as the Get Directions button:
               a raw neon color can wash out white text just as easily as
               it washes out black. */
            background: var(--gp-pin-color, var(--gp-accent));
            background: color-mix(in srgb, var(--gp-pin-color, var(--gp-accent)) 75%, #04121a 25%);
            color: #ffffff;
            font-size: 12px;
            font-weight: 700;
            line-height: 1.3;
            text-align: center;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }
        .granpai-pin-tag-img + .granpai-pin-tag-text {
            border-radius: 0 0 8px 8px;
        }
        /* Speech-bubble tail: a small rotated square tucked just behind
           the body's bottom edge, same fill color, with a touch of its
           own corner rounding so the point isn't a razor-sharp corner -
           reads as a single continuous shape with the body instead of a
           separate triangle glued on. */
        .granpai-pin-tag::after {
            content: '';
            position: absolute;
            top: 100%;
            left: 50%;
            width: 11px;
            height: 11px;
            margin-top: -7px;
            transform: translateX(-50%) rotate(45deg);
            border-radius: 0 0 3px 0;
            background: color-mix(in srgb, var(--gp-pin-color, var(--gp-accent)) 75%, #04121a 25%);
        }
        .granpai-pulse-marker:hover .granpai-pin-tag {
            opacity: 1;
            transform: translate(-50%, 0) scale(1);
        }
        /* Triggered by JS on next/prev navigation (see navigatePin) -
           a slightly springier pop than the plain hover fade-in, so it
           reads as 'look here now' rather than a passive hover state. */
        .granpai-pin-tag.granpai-pin-tag-force-show {
            opacity: 1;
            animation: granpai-pin-tag-bounce 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
        }
        @keyframes granpai-pin-tag-bounce {
            0%   { transform: translate(-50%, 10px) scale(0.8); opacity: 0; }
            60%  { transform: translate(-50%, -4px) scale(1.05); opacity: 1; }
            100% { transform: translate(-50%, 0) scale(1); opacity: 1; }
        }

        .granpai-flatmap {
            position: absolute;
            inset: 0;
            /* --gp-surface, not --gp-bg: --gp-bg is intentionally
               transparent for the light theme (fine for Globe mode,
               where the sphere itself is the visual surface regardless
               of what is behind it) - but flat map fills its entire
               rectangular box with no shape of its own, so a
               transparent background there just showed whatever is
               behind the page for BOTH themes, making light and dark
               look nearly identical. --gp-surface is the theme's actual
               solid-color surface (#ffffff light / #0a0c18 dark). */
            background: var(--gp-wrapper-bg, var(--gp-surface, transparent));
            overflow: hidden;
            cursor: grab;
            touch-action: none;
        }
        .granpai-flatmap.is-dragging {
            cursor: grabbing;
        }
        .granpai-flatmap svg {
            display: block;
            width: 100%;
            height: 100%;
        }
        /* Flight line between two pins on Next/Prev/click, flat-map
           version (see drawArc() inside initFlatMode() in globe-init.js). */
        .granpai-arc-overlay {
            position: absolute;
            inset: 0;
            width: 100%;
            height: 100%;
            pointer-events: none;
            overflow: visible;
            z-index: 2;
        }
        .granpai-arc-line {
            stroke-width: 2;
            stroke-dasharray: 5 4;
            fill: none;
            opacity: 0;
        }
        .granpai-arc-line.is-animating {
            animation:
                granpai-arc-flow 2.2s linear forwards,
                granpai-arc-fade 2.2s ease-out forwards;
        }
        @keyframes granpai-arc-flow {
            from { stroke-dashoffset: 0; }
            to   { stroke-dashoffset: -54; }
        }
        @keyframes granpai-arc-fade {
            0%   { opacity: 0; }
            15%  { opacity: 0.75; }
            75%  { opacity: 0.75; }
            100% { opacity: 0; }
        }

        /* Timeline (Pro) - see updateTimelinePins()/initTimelineControls()
           in globe-init.js. Sits as a floating bar at the bottom of the
           stage, identical positioning whether the map underneath is
           Globe or Flat Map - it's a sibling UI element, not tied to
           either mode's own rendering. */
        .granpai-timeline-bar {
            flex: 0 0 auto;
            display: flex;
            align-items: center;
            gap: 14px;
            padding: 10px 16px;
            margin-top: 8px;
            border-radius: 12px;
            background: var(--gp-surface, rgba(10, 12, 24, 0.65));
            color: var(--gp-text, #ffffff);
        }
        /* Makes the hidden attribute actually take effect - without
           this explicit rule, .granpai-timeline-bar's own display:flex
           above (an author style) always wins over the browser's
           default [hidden] { display: none } (a user-agent style),
           REGARDLESS of specificity or whether JS ever sets/clears the
           attribute at all - the bar was always visible on every map,
           Timeline enabled or not, purely from this CSS rule existing. */
        .granpai-timeline-bar[hidden] {
            display: none;
        }
        .granpai-timeline-play {
            position: relative;
            flex: 0 0 auto;
            width: 34px;
            height: 34px;
            border-radius: 50%;
            border: none;
            /* Blends in some dark regardless of how bright the
               configured pin color is - same reasoning as the Get
               Directions button: a light/neon pin color (including the
               default cyan accent) can wash out the white icon just as
               easily as it would wash out a dark one, so this
               guarantees contrast instead of assuming the pin color is
               always dark enough on its own. */
            background: linear-gradient(135deg,
                color-mix(in srgb, var(--gp-pin-color, var(--gp-accent)) 80%, #04121a 20%),
                color-mix(in srgb, var(--gp-pin-color, var(--gp-accent)) 55%, #04121a 45%));
            box-shadow: 0 2px 10px -2px var(--gp-pin-color, var(--gp-accent));
            color: #ffffff;
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            padding: 0;
            transition: transform 0.15s ease, box-shadow 0.15s ease;
        }
        .granpai-timeline-play:hover {
            transform: scale(1.08);
            box-shadow: 0 4px 16px -2px var(--gp-pin-color, var(--gp-accent));
        }
        .granpai-timeline-play:active {
            transform: scale(0.96);
        }
        /* Without this, clicking/tabbing to the button shows the
           browser's own default focus outline, which follows the
           element's rectangular bounding box regardless of
           border-radius - producing a square ring around this round
           button. This box-shadow ring is shaped correctly instead,
           since box-shadow (unlike outline in most browsers) respects
           the element's own border-radius. */
        .granpai-timeline-play:focus-visible {
            outline: none;
            box-shadow: 0 0 0 3px color-mix(in srgb, var(--gp-pin-color, var(--gp-accent)) 50%, transparent 50%);
        }
        /* A soft, slow pulse ring while idle - an invitation to press
           Play, not just a static icon waiting to be noticed. Stops the
           instant playback actually starts (.is-playing below), so it
           never looks like something's wrong once it's running. */
        .granpai-timeline-play::after {
            content: '';
            position: absolute;
            inset: -4px;
            border-radius: 50%;
            border: 1.5px solid var(--gp-pin-color, var(--gp-accent));
            opacity: 0;
            animation: granpai-timeline-play-pulse 2.4s ease-out infinite;
        }
        .granpai-timeline-play.is-playing::after {
            animation: none;
            opacity: 0;
        }
        @keyframes granpai-timeline-play-pulse {
            0%   { transform: scale(0.9); opacity: 0.6; }
            100% { transform: scale(1.35); opacity: 0; }
        }
        .granpai-timeline-play svg {
            margin-left: 1px;
            position: relative;
            z-index: 1;
        }
        .granpai-timeline-play.is-playing svg {
            margin-left: 0;
        }
        .granpai-timeline-nodes {
            position: relative;
            flex: 1 1 auto;
            min-width: 0;
            height: 46px;
            display: flex;
            align-items: center;
            gap: 34px;
            overflow-x: auto;
            cursor: grab;
            /* Drag/swipe with a natural snap-to-year settle, instead of
               free-scrolling to an arbitrary in-between position - the
               same interaction an iOS date wheel or Spotify's year
               picker uses. */
            scroll-snap-type: x mandatory;
            -webkit-overflow-scrolling: touch;
            scrollbar-width: none;
        }
        .granpai-timeline-nodes.is-dragging {
            cursor: grabbing;
            scroll-snap-type: none;
        }
        .granpai-timeline-nodes::-webkit-scrollbar {
            display: none;
        }
        /* The ruler baseline itself - a single line running the full
           width, behind every node, with each node's own tick mark (see
           .granpai-timeline-node::before) sitting on top of it. Reads as
           a real timeline axis rather than a loose row of floating
           labels. */
        .granpai-timeline-nodes {
            background-image: linear-gradient(var(--gp-grid-line, rgba(255,255,255,0.14)), var(--gp-grid-line, rgba(255,255,255,0.14)));
            background-repeat: no-repeat;
            background-position: center 6px;
            background-size: 100% 1px;
        }
        /* Lets the FIRST and LAST year also reach dead-center when
           scrolled all the way to either end - without this, the
           carousel could only center years somewhere in the middle of
           the list, never the ones at the very edges. */
        .granpai-timeline-nodes::before,
        .granpai-timeline-nodes::after {
            content: '';
            flex: 0 0 auto;
            width: calc(50% - 17px);
        }
        .granpai-timeline-node {
            position: relative;
            scroll-snap-align: center;
            flex: 0 0 auto;
            border: none;
            background: transparent;
            cursor: pointer;
            padding: 15px 3px 0;
            font-weight: 700;
            white-space: nowrap;
            color: var(--gp-text-muted, #8a8fa3);
            font-size: 12px;
            opacity: 0.4;
            transition: font-size 0.2s ease, opacity 0.2s ease, color 0.2s ease;
        }
        /* Each year's own tick mark on the ruler baseline - taller and
           brighter for the centered/active year, so its position on the
           axis is unmistakable even before reading the number itself. */
        .granpai-timeline-node::before {
            content: '';
            position: absolute;
            top: 0;
            left: 50%;
            transform: translateX(-50%);
            width: 1.5px;
            height: 7px;
            background: var(--gp-text-muted, #8a8fa3);
            opacity: 0.6;
            transition: height 0.2s ease, background 0.2s ease, opacity 0.2s ease;
        }
        .granpai-timeline-node:hover {
            opacity: 0.7;
        }
        /* The one centered year - larger and in the accent color, every
           other year stays small/dim (see .granpai-timeline-node's own
           base style above) so the current position is unmistakable at
           a glance. */
        .granpai-timeline-node.is-active {
            color: #ffffff;
            opacity: 1;
            font-size: 19px;
        }
        .granpai-timeline-node.is-active::before {
            height: 12px;
            background: var(--gp-pin-color, var(--gp-accent));
            opacity: 1;
        }
        .granpai-timeline-label {
            display: none;
        }
        /* Flat Map's own zoom controls sit in the bottom-right corner
           (see .granpai-flatmap-zoom-controls below) - narrow the bar
           on that side so the two never overlap. Globe mode has nothing
           in that corner, so it keeps the full-width bar. */
        .granpai-flatmap-zoom-controls {
            position: absolute;
            right: 14px;
            bottom: 14px;
            z-index: 3;
            display: flex;
            flex-direction: column;
            gap: 6px;
            pointer-events: auto;
        }
        .granpai-zoom-btn {
            width: 30px;
            height: 30px;
            border-radius: 6px;
            border: 1px solid var(--gp-grid-line);
            background: var(--gp-surface, #0a0c18);
            color: var(--gp-text, #fff);
            font-size: 16px;
            line-height: 1;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        .granpai-zoom-btn:hover {
            border-color: var(--gp-accent);
            color: var(--gp-accent);
        }
        .granpai-zoom-reset {
            font-size: 14px;
        }
        .granpai-flatmap .granpai-land {
            fill: var(--gp-land-fill);
            stroke: var(--gp-land-stroke);
            stroke-width: 0.6;
        }
        .granpai-flatmap-markers {
            position: absolute;
            inset: 0;
            pointer-events: none;
        }
        .granpai-flatmap-markers .granpai-pulse-marker {
            position: absolute;
        }
        .granpai-flatmap-loading {
            position: absolute;
            inset: 0;
            display: flex;
            align-items: center;
            justify-content: center;
            color: var(--gp-text-muted);
            font-size: 13px;
            letter-spacing: 0.5px;
            text-align: center;
            padding: 20px;
        }
