/* Map builder — app styles (from design/mockup-figma-v2.html) */
:root{
  --ink:#1e2328; --dim:#6b7480; --faint:#9aa3ad;
  --panel:#ffffff; --canvas:#eef0f2;
  --line:#e5e8eb; --line-2:#d7dbe0;
  --hover:#f4f6f8;
  --accent:#2d7ff9; --accent-soft:rgba(45,127,249,.10); --accent-line:rgba(45,127,249,.5);
  --r:7px;
  --font:-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  --topH:48px; --railW:60px; --leftW:200px; --rightW:240px;
}
*{box-sizing:border-box;margin:0;padding:0}
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
body{touch-action:manipulation;font-family:var(--font);color:var(--ink);height:100vh;overflow:hidden;background:var(--canvas);font-size:13px}
button{font:inherit}
.ico{display:block;width:15px;height:15px;stroke:currentColor;stroke-width:1.7;fill:none;stroke-linecap:round;stroke-linejoin:round}
.ico.sm{width:13px;height:13px}
.ico.md{width:18px;height:18px}
/* js/icons.js ICON_FILL — the two Phosphor fill-style icons (line/freeform)
   are solid shapes, not outlines, unlike every other entry in this set. */
.ico.ico-fill{fill:currentColor;stroke:none}

/* ---------- top nav ---------- */
.top{position:fixed;top:0;left:0;right:0;height:var(--topH);background:var(--panel);border-bottom:1px solid var(--line);display:flex;align-items:center;gap:4px;padding:0 12px 0 0;z-index:40}
.logo{display:flex;align-items:center;gap:8px;font-weight:700;font-size:13.5px;letter-spacing:-.01em;width:calc(var(--railW) + var(--leftW));height:100%;padding:0 12px;flex:none;color:inherit;text-decoration:none}
.logo img{width:20px;height:20px;border-radius:50%}
.betatag{font-size:9.5px;font-weight:700;letter-spacing:.03em;text-transform:uppercase;color:var(--dim);background:var(--hover);border:1px solid var(--line-2);border-radius:999px;padding:1px 6px}
/* .loadedchip/.lm-* kept for the design/ mockups that still reference them —
   the live top bar replaced the chip with .doctitle below (2026-07-19,
   planning/feature-map-modes.md) */
.loadedchip{display:flex;align-items:center;gap:2px;max-width:220px;background:var(--accent-soft);border:1px solid var(--accent-line);border-radius:999px;padding:3px 4px 3px 10px;flex:none}
.loadedchip[hidden]{display:none}
.lm-name{font-size:12px;font-weight:600;color:var(--accent);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.lm-x{display:grid;place-items:center;width:18px;height:18px;border:none;background:none;border-radius:50%;color:var(--accent);cursor:pointer;padding:0}
.lm-x:hover{background:var(--accent-soft)}
.lm-x .ico{width:12px;height:12px}
/* Mode 2 — a saved map is open (js/mymaps.js setCurrent): branding shrinks
   to the bare icon, centered over the rail column below it (width:--railW +
   centering matches .railbtn's own, verified in the reference mockup
   design/mapname-option-b2-icon-title.html); wordmark + Beta tag drop with
   it and the map's name takes their place as an editable doc title. */
.logo.icononly{width:var(--railW);justify-content:center;padding:0}
.logo.icononly .logotext,.logo.icononly .betatag{display:none}
.logo.icononly img{width:22px;height:22px}
.doctitle{display:flex;align-items:center;height:32px;padding:0 10px 0 8px;border-radius:6px;cursor:text;max-width:320px;flex:none}
.doctitle[hidden]{display:none} /* display:flex would beat the hidden attr */
.doctitle:hover{background:var(--hover)}
.doctitle .dt-name{font-size:11px;font-weight:650;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* mid-edit: the name swaps out for a real input (js/mymaps.js) */
.doctitle.editing:hover{background:none}
.doctitle.editing .dt-name{display:none}
.dt-input{font-size:14px;font-weight:650;color:var(--ink);border:none;outline:none;background:#fff;box-shadow:0 0 0 2px var(--accent);border-radius:5px;padding:2px 6px;width:220px;font-family:inherit}
/* the title's actions menu (Rename / Duplicate / Delete) off a small chevron */
.dt-chev{width:24px;height:24px;border:none;background:none;border-radius:6px;color:var(--dim);cursor:pointer;display:grid;place-items:center;padding:0}
.dt-chev:hover{background:var(--hover);color:var(--ink)}
#doctitle-menu{top:32px}
.vsep{width:1px;height:22px;background:var(--line);margin:0 6px}
.navicon{width:32px;height:32px;border:none;background:none;border-radius:6px;color:var(--ink);display:grid;place-items:center;cursor:pointer}
.navicon:hover{background:var(--hover)}
.navicon.on{background:var(--accent-soft);color:var(--accent)}
.navicon.locked{opacity:.45}
.navicon:disabled{color:var(--line-2);cursor:default}
.navicon:disabled:hover{background:none}
.spacer{flex:1}
/* .savebtn: unused live (Save is a plain .navicon now, next to Help) — kept
   only because several design/ mockups still link the real app.css and
   reference it (same convention as .loadedchip). */
.savebtn{height:32px;width:96px;border-radius:6px;background:var(--hover);border:1px solid var(--line-2);color:var(--ink);font-size:13px;font-weight:600;cursor:pointer}
.savebtn:hover{background:var(--line-2)}
/* Publish + Download share one width so they read as a paired action, not
   two differently-sized buttons — Publish is the lower-emphasis (outlined,
   no fill) of the pair, Download stays the one solid/primary action. */
.navbtn.publish{height:32px;width:80px;padding:0;border-radius:6px;background:none;border:1px solid var(--accent-line);color:var(--accent);font-size:13px;font-weight:600;cursor:pointer}
.navbtn.publish:hover{background:var(--accent-soft)}
/* Terrain has no publish option — grayed but still clickable (unlike a real
   disabled button) so the click still opens #pubtip explaining why. */
.navbtn.publish.disabled{opacity:.45}
.navbtn.publish.disabled:hover{background:none}
/* Anchored under the top bar itself (js/codeview.js positions it against
   .btn-publish/.top) rather than .locktip's usual .maptools-relative
   top:42px — same box styling, different anchor. */
#pubtip{top:100%;margin-top:6px}
.navbtn.export{height:32px;width:80px;padding:0;border-radius:6px;border:none;background:var(--accent);color:#fff;font-size:13px;font-weight:600;cursor:pointer}
.navbtn.export:hover{filter:brightness(1.06)}
.acct{width:32px;height:32px;border-radius:50%;border:none;padding:0;cursor:pointer;background:linear-gradient(135deg,#2d7ff9,#7db4ff);color:#fff;display:grid;place-items:center;font-size:11px;font-weight:640}
.acct.loggedout{width:auto;height:32px;padding:0 14px;border-radius:6px;background:none;border:1px solid var(--accent-line);color:var(--accent);font-size:12.5px;font-weight:600}
.acct.loggedout:hover{background:var(--accent-soft)}

.navdrop{position:relative}
.dropmenu{position:absolute;top:36px;left:0;background:#fff;border:1px solid var(--line);border-radius:8px;box-shadow:0 8px 24px rgba(28,44,60,.16);padding:4px;min-width:150px;z-index:60}
/* flex, not block: an icon+text item (data-icon span + label, e.g.
   #doctitle-menu/#mymaps-rowmenu/#overlay-add-menu) needs them on one row,
   not stacked — a plain text-only item (e.g. #menu-account/#menu-language)
   renders the same either way, so one rule covers every .dropmenu instance
   rather than each icon-bearing menu needing its own ID-scoped override. */
.dropmenu button{display:flex;align-items:center;gap:8px;width:100%;text-align:left;padding:7px 10px;border:none;background:none;border-radius:5px;cursor:pointer;font-size:12.5px}
/* This author rule's (0,1,1) otherwise beats the UA [hidden]{display:none}
   (0,1,0) and silently shows an admin-only item to everyone — same bug
   class as .dup-badge's incident (decisions.md, 2026-08-21). */
.dropmenu button[hidden]{display:none}
.dropmenu button:hover{background:var(--hover)}
/* .dropmenu button's reset (0,1,1) otherwise beats .mini-btn (0,1,0) and
   silently strips its accent background/color — restore it here. */
.dropmenu button.mini-btn{background:var(--accent);color:#fff;text-align:center}
.dropmenu button.mini-btn:hover{background:var(--accent)}
.dropmenu button.on{color:var(--accent);font-weight:600}
/* Named "align-right" not "right" — a plain .right class collides with the
   #inspector panel's .right rule below (same specificity, later in source,
   so it silently won: position:fixed + bottom:0 + width:288px stretched any
   .dropmenu.right dropdown to the full viewport height). */
.dropmenu.align-right{left:auto;right:0}
.dm-section-label{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--dim);padding:8px 6px 4px}

/* account menu */
.acctmenu{width:230px;max-height:75vh;overflow-y:auto;padding:10px}
.acct-form{display:flex;flex-direction:column;gap:8px}
.acct-form input{height:32px;border:1px solid var(--line-2);border-radius:6px;padding:0 10px;font:inherit;font-size:12.5px;outline:none;width:100%}
.acct-form input:focus{border-color:var(--accent-line)}
.acct-row{display:flex;gap:6px}
.acct-row .mini-btn{flex:1}
.mini-btn.ghost{background:none;color:var(--accent);border:1px solid var(--accent-line)}
.acct-google{display:flex;align-items:center;justify-content:center;gap:7px;height:32px;border:1px solid var(--line-2);border-radius:6px;background:none;font-size:12.5px;font-weight:600;cursor:pointer;width:100%}
.acct-google:hover{background:var(--hover)}
.acct-sheet-head{display:none}
.acct-info{padding:2px 4px 8px;border-bottom:1px solid var(--line);margin-bottom:6px}
.acct-email{font-size:12.5px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.acct-tier{font-size:11.5px;color:var(--dim);margin-top:2px}
/* One class, not a list of ids. "My orders" was added to this menu without
   being added to the id list this rule used to carry, so that one row alone
   fell through to .dropmenu button's 10px padding and sat 4px right of every
   item above it (Aaron's report). A class cannot be forgotten the same way.
   Still out-specifies .dropmenu button (0,2,0 vs 0,1,1), which is what makes
   display:block and the 6px padding stick. */
.acctmenu .acct-item{display:block;width:100%;text-align:left;padding:7px 6px;border:none;background:none;border-radius:5px;cursor:pointer;font-size:12.5px}
.acctmenu .acct-item:hover{background:var(--hover)}
.acct-cta{width:100%;margin-bottom:4px}
.acct-hint{padding:0 2px 8px}
.acctmenu .acct-row{margin-bottom:6px}
.acct-legal{border-top:1px solid var(--line);margin-top:6px;padding:8px 4px 2px;font-size:11px;color:var(--faint)}
.acct-legal a{color:var(--dim);text-decoration:none}
.acct-legal a:hover{text-decoration:underline}

/* help/use-cases menu — same dropmenu base as the account menu. A 2-column
   card grid of the 8 SEO use-case pages (emoji instead of the Lucide icon
   set — cheaper to keep in sync as pages are added/renamed, and reads
   warmer for a "solutions" menu than the app's usual line-icon style),
   plus "How to use" pinned at the bottom in the same muted, border-top
   style .acct-legal already uses for Help/Terms/Privacy in the account
   menu (2026-07-23 design review — design/helpmenu-footer-index.html). */
.helpmenu{width:420px;max-height:80vh;overflow-y:auto;padding:10px}
.helpmenu .dm-section-label:first-child{padding-top:4px}
.helpmenu-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}
.helpmenu-grid a{display:flex;flex-direction:column;align-items:flex-start;gap:6px;padding:12px;border:1px solid var(--line);border-radius:8px;font-size:12px;line-height:1.3;color:inherit;text-decoration:none}
.helpmenu-grid a:hover{background:var(--hover);border-color:var(--line-2)}
.helpmenu-grid a .emoji{font-size:18px;line-height:1}
/* A separate full-bleed line, not a border on the row itself — negative
   margin cancels .helpmenu's own 10px padding so the line runs truly edge
   to edge, independent of .helpmenu-howto's padding below it (mixing the
   two, via a border-top + negative margin on the row itself, was what
   broke left-alignment with the grid last time). */
.helpmenu-divider{height:1px;background:var(--line);margin:10px -10px 0}
/* Sibling of .helpmenu-grid, not nested inside it, so it's already full
   width and naturally left/right-aligned with the grid above. Equal
   padding on every side, color --ink (matches the rest of the app's body
   text) instead of --faint, per Aaron's call. */
.helpmenu-howto{padding:12px 12px 12px 17px;border-radius:8px;font-size:11.5px;color:var(--ink);display:flex;align-items:center;gap:6px;text-decoration:none}
.helpmenu-howto:hover{background:var(--hover)}
.helpmenu-howto .ico{width:15px;height:15px}
.helpmenu-howto .emoji{font-size:13px;line-height:1}

/* ---------- notifications (js/notifications.js) ---------- */
/* Positioned against #drop-notif-wrap (.navdrop already position:relative),
   not #btn-notif itself — one fewer thing to keep in sync if the icon's own
   box model ever changes. */
.notif-dot{position:absolute;top:2px;right:2px;width:8px;height:8px;border-radius:50%;background:#e0433b;border:1.5px solid var(--panel);pointer-events:none}
.notifmenu{width:340px;max-height:70vh;overflow-y:auto;padding:6px}
.notif-compose-btn{display:block;width:100%;text-align:left;padding:8px 10px;margin-bottom:2px;border:none;border-radius:6px;background:var(--accent-soft);color:var(--accent);font-size:12.5px;font-weight:600;cursor:pointer}
.notif-compose-btn:hover{background:var(--accent-line)}
.notif-compose-btn[hidden]{display:none}
.notif-list{display:flex;flex-direction:column;gap:1px}
.notif-row{display:flex;flex-direction:column;align-items:flex-start;gap:2px;width:100%;text-align:left;padding:9px 10px;border:none;background:none;border-radius:6px;cursor:pointer;position:relative}
.notif-row:hover{background:var(--hover)}
.notif-row-title{font-size:12.5px;color:var(--ink);padding-left:0;line-height:1.4}
.notif-row.unread .notif-row-title{font-weight:700;padding-left:13px}
.notif-row.unread .notif-row-title::before{content:'';position:absolute;left:10px;top:13px;width:6px;height:6px;border-radius:50%;background:var(--accent)}
.notif-row-time{font-size:10.5px;color:var(--faint)}
.notif-empty{padding:16px 10px;text-align:center;font-size:12px;color:var(--faint)}
.notif-empty[hidden]{display:none}

/* Free-export admin viewer (js/freeexports.js) — same dropmenu/list shell as
   the notification bell above, with a thumbnail added per row. */
.freeexpmenu{width:360px;max-height:70vh;overflow-y:auto;padding:6px}
.freeexp-tabs{display:flex;gap:2px;margin:2px 0 6px;padding:0 2px}
.freeexp-tab{flex:1;padding:4px 0;border:none;background:none;border-radius:6px;font-size:11.5px;color:var(--dim);cursor:pointer}
.freeexp-tab:hover{background:var(--hover)}
.freeexp-tab.on{background:var(--hover);color:var(--ink);font-weight:600}
.freeexp-list{display:flex;flex-direction:column;gap:2px}
.freeexp-row{display:flex;align-items:center;gap:9px;width:100%;text-align:left;padding:6px;border:none;background:none;border-radius:6px;cursor:pointer}
.freeexp-row:hover{background:var(--hover)}
.freeexp-row.busy{opacity:.55;cursor:progress}
.freeexp-thumb{width:64px;height:32px;object-fit:cover;border-radius:4px;border:1px solid var(--line-2);flex:none;background:var(--canvas)}
/* rows with no preview image show their kind in the thumbnail slot instead */
.freeexp-kind{display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:600;letter-spacing:.03em;text-transform:uppercase;color:var(--dim)}
.freeexp-kind.k-paid{color:#1a7f37;background:#e9f6ec}
.freeexp-kind.k-published{color:#0b5cad;background:#e8f1fb}
.freeexp-kind.k-poster,.freeexp-kind.k-order{color:#8a4b08;background:#fbf0e2}
.freeexp-meta{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1}
.freeexp-whorow{display:flex;align-items:center;gap:6px;min-width:0}
.freeexp-who{font-size:12px;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
.freeexp-tier{flex:none;font-size:9.5px;line-height:1;padding:2px 5px;border-radius:8px;color:var(--faint);border:1px solid var(--line-2)}
.freeexp-tier.paid{color:#1a7f37;border-color:#1a7f37;font-weight:600}
.freeexp-sub{font-size:10.5px;color:var(--faint);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.freeexp-sub.bad{color:#c62828}

/* Detail modal (2026-08-21): "from Aaron" personal-note treatment, not a
   generic system-confirm shell — every notification really is Aaron writing
   in, so the header says that instead of a plain title bar. */
.notifdlg{width:440px}
.notif-from{display:flex;align-items:center;gap:8px;padding:20px 22px 0}
.notif-avatar{flex:none;width:38px;height:38px;border-radius:50%;object-fit:cover;display:block}
.notif-from-text{flex:1;min-width:0}
.notif-from-name{font-size:12.5px;font-weight:700;color:var(--ink)}
.notif-from-sub{font-size:11.5px;color:var(--faint)}
.notifdlg .dlg-close{margin-top:-2px}
.notif-detail-title{padding:16px 22px 0;font-size:17px;font-weight:750;line-height:1.35;letter-spacing:-.01em;color:var(--ink)}
.notifdlg .dlg-body{padding:10px 22px 22px}
.notifdlg .confirmtext{font-size:14px;line-height:1.65;color:var(--dim);white-space:pre-line}
.notifdlg .dlg-foot{border-top:none;padding:0 22px 22px}
.notif-detail-actions .mini-btn.ghost{background:var(--accent-soft);border-color:var(--accent-line)}

.dlg-foot.end{justify-content:flex-end}
.dlg-label{display:block;font-size:11.5px;font-weight:650;color:var(--dim);margin:12px 0 5px}
.dlg-label:first-child{margin-top:0}
.dlg-label-opt{font-weight:400;text-transform:none;letter-spacing:0;color:var(--faint)}
.dlg-textarea{height:72px;padding:8px 10px;resize:vertical;line-height:1.4}
.notif-compose-btnrow{display:flex;gap:8px;margin-bottom:6px}
.notif-compose-btnrow .dlg-input:first-child{flex:0 0 40%}
/* ---------- Print mode (planning/feature-print-mode.md) ---------- */
/* The whole mode re-themes by overriding the SAME custom properties app.css
   already defines on :root, scoped to body.print-mode — every component
   keyed off var(--accent) (rail, rows, focus rings, sliders, seg pills)
   repaints for free, with no per-component print variants to keep in sync.
   #14b898 is Aaron's exact pick (§9.6). */
body.print-mode,body.print-boot{--accent:#14b898;--accent-soft:rgba(20,184,152,.10);--accent-line:rgba(20,184,152,.5)}
/* .acct's signed-in avatar is the one component whose colour is a hardcoded
   gradient rather than var(--accent) (.acct.loggedout's border/text already
   re-themes for free, same as everything else) — Aaron: "change profile's
   background color to green in print mode". :not(.loggedout) so the
   signed-out "Log in" pill (background:none, line 86) is untouched. */
body.print-mode .acct:not(.loggedout){background:linear-gradient(135deg,#14b898,#6fe0c8)}

/* Centered Map/Print switch. Deliberately not .seg (a filter pill) — this is
   a mode switch, so it gets the segmented-control look instead. */
/* Absolutely centred on .top, not flexed between spacers: the nav's right
   side is a different width in each mode (Publish+Download vs Purchase), so
   a flexed toggle slid sideways on every switch (Aaron, 2026-08-23). */
.modetoggle{position:absolute;left:50%;transform:translateX(-50%);display:flex;background:var(--hover);border-radius:999px;padding:3px;gap:2px;flex:none;z-index:1}
/* REQUIRED for the pre-launch hide in map.html: [hidden] is display:none at
   author level, which loses to the display:flex above. The rail hit exactly
   this and shipped all nine tabs at once for a moment. */
.modetoggle[hidden]{display:none}
.modetoggle button{border:none;background:none;padding:6px 18px;border-radius:999px;font:650 12.5px/1 inherit;font-family:inherit;color:var(--dim);cursor:pointer}
.modetoggle button.on{background:var(--panel);color:var(--ink);box-shadow:0 1px 3px rgba(20,30,45,.18)}

/* Purchase: same flat-pill geometry as .navbtn.export (Download) so the two
   read as the same class of action, in the mode's own accent (§9.7).
   Download's exact 80px too (Aaron, 2026-09-07) — it was 164px back when the
   label carried the price ("Purchase · $9.99"), which it hasn't since the
   price moved into the buy dialog. The .price span that width existed for is
   gone with it. #btn-mapedit-done ("Done") wears this class as well. */
.navbtn.purchase{min-width:80px;height:32px;padding:0 14px;white-space:nowrap;flex:none;border-radius:6px;border:none;background:var(--accent);color:#fff;font-size:13px;font-weight:600;cursor:pointer}
.navbtn.purchase:hover{filter:brightness(1.06)}
.navbtn.purchase[hidden]{display:none}
/* the unframed poster price inside Purchase (js/printbuy.js paintPurchasePrice) */
.navbtn.purchase .pp-price{font-weight:600;opacity:.85}

/* Poster purchase dialog. The two ways to pay are cards rather than a radio
   list: this is the moment someone decides to spend money, and one glance
   should answer "what do I get and what does it cost". The credits card is
   only in the DOM when they have credits (js/printpay.js) — see there for
   why a first-time buyer is never shown one. */
.poster-buy{max-width:420px}
.poster-buy-spec{font-size:13px;color:var(--dim);margin-bottom:14px}
.poster-buy-opts{display:flex;flex-direction:column;gap:8px}
.poster-opt{display:grid;grid-template-columns:auto auto 1fr;grid-template-rows:auto auto;
  gap:0 10px;align-items:center;text-align:left;width:100%;padding:12px 14px;
  border:1px solid var(--line);border-radius:8px;background:var(--panel);cursor:pointer}
.poster-opt:hover{border-color:var(--accent-line);background:var(--accent-soft)}
/* Selected, not submitted: Confirm is what spends. The ring has to read as
   "this is the one" strongly enough that nobody presses Confirm expecting the
   other one. */
.poster-opt.sel{border-color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent);background:var(--accent-soft)}
.poster-opt:disabled{opacity:.55;cursor:default}
.poster-opt-ic{grid-row:1/3;grid-column:1;display:flex;align-items:center;color:var(--accent)}
.poster-opt-ic .ico{width:22px;height:22px}
.poster-opt-price{grid-row:1;grid-column:2;font-size:16px;font-weight:700;color:var(--accent)}
.poster-opt-label{grid-row:1;grid-column:3;font-size:14px;font-weight:600;color:var(--ink)}
.poster-opt-sub{grid-row:2;grid-column:2/4;font-size:12px;color:var(--dim);margin-top:2px}
/* Shown while the Firestore profile is still loading — we can't pick the
   right options (Business = included, else cash/credits) until it lands. */
.poster-buy-wait{font-size:13px;color:var(--dim);padding:12px 2px}
.poster-buy-fairuse{margin-top:14px;padding-top:12px;border-top:1px solid var(--line-2);
  font-size:11.5px;line-height:1.5;color:var(--dim)}
.poster-buy-fairuse p{margin:0 0 6px}
.poster-buy-fairuse p:last-child{margin-bottom:0}
.poster-buy-fairuse strong{color:var(--ink);font-weight:600}
.poster-buy-fairuse a{color:var(--accent)}
/* ---- the purchase dialog (js/printbuy.js, design/checkout-index.html) ----
   ONE modal: preview left, format and its options right, running total in the
   footer. It replaced a fork modal plus a four-step wizard on 2026-09-02.
   Two columns collapse to one under 700px, where the preview goes first so the
   poster is still the first thing you see. */
/* `.dialog{width:430px}` is defined LATER in this file, so a plain .buydlg
   rule loses to it on source order at equal specificity — the same trap
   #pimport-dialog documents below. ID specificity is the fix. */
#buy-scrim .dialog{width:720px;max-width:94vw}
.buy-body{display:grid;grid-template-columns:250px 1fr}
.buy-preview{background:var(--canvas);border-right:1px solid var(--line-2);
  padding:20px 18px;display:flex;flex-direction:column;align-items:center;justify-content:center}
.buy-sheet{border-radius:2px;box-shadow:0 6px 20px rgba(15,25,40,.18);transition:padding .15s,background .15s}
.buy-sheet.framed{box-shadow:0 8px 24px rgba(15,25,40,.28)}
/* a canvas has depth and no frame: a solid offset edge reads as the stretcher bars */
.buy-sheet.buy-canvas{box-shadow:5px 6px 0 -1px #d6cfbd,8px 12px 22px rgba(15,25,40,.26)}
.buy-poster{position:relative;background:var(--panel);overflow:hidden}
.buy-poster img{display:block;width:100%;height:100%;object-fit:cover}
.buy-posterwait{position:absolute;inset:0;background:
  linear-gradient(100deg,var(--line-2) 30%,var(--line) 50%,var(--line-2) 70%);
  background-size:220% 100%;animation:buyshim 1.1s linear infinite}
@keyframes buyshim{to{background-position:-220% 0}}
/* Where the cut can land. There is no bleed in the print file, so this is a
   real warning rather than decoration. */
.buy-trim{position:absolute;border:1px dashed rgba(255,255,255,.8);
  outline:1px dashed rgba(0,0,0,.28);pointer-events:none}
.buy-pvmeta{margin-top:12px;font-size:11.5px;color:var(--dim);text-align:center;line-height:1.5}
.buy-pvmeta b{color:var(--ink)}

.buy-opts{padding:16px 18px;min-width:0}
/* format switch: one row instead of two cards, so size and frame never scroll */
.buy-seg{display:flex;background:var(--hover,#f1f3f5);border-radius:9px;padding:3px;margin-bottom:14px}
.buy-seg button{flex:1;border:0;background:none;border-radius:7px;padding:8px 6px;font:inherit;
  font-size:12.5px;font-weight:600;color:var(--dim);cursor:pointer;min-width:0}
.buy-seg button.sel{background:var(--panel);color:var(--ink);box-shadow:0 1px 3px rgba(15,25,40,.14)}
.buy-seg button:disabled{opacity:.5;cursor:default}
.buy-seg button small{display:block;font-size:10.5px;font-weight:500;color:var(--faint);margin-top:1px}
.buy-seg button.sel small{color:var(--accent)}

.buy-lab{font-size:10px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;
  color:var(--faint);margin:14px 0 7px}
.buy-lab:first-child{margin-top:0}
.buy-note{font-size:11px;color:var(--dim);line-height:1.5;margin-top:7px}

/* pay routes. The amount sits on its own line above the label: a sub-line like
   "Unlimited poster downloads, plus 20 export credits a month" leaves no room
   to put a price opposite it. */
.buy-pays{display:flex;flex-direction:column;gap:7px}
.buy-pay{width:100%;text-align:left;border:1px solid var(--line);border-radius:9px;
  padding:9px 11px;background:var(--panel);cursor:pointer;display:block}
.buy-pay:hover{border-color:var(--accent-line)}
.buy-pay.sel{border-color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent);background:var(--accent-soft)}
.buy-pay .amt{display:block;font-size:15px;font-weight:700;color:var(--accent);line-height:1.15}
.buy-pay .l{display:block;font-size:12.5px;font-weight:600;margin-top:2px;color:var(--ink)}
.buy-pay .s{display:block;font-size:10.5px;color:var(--dim);margin-top:1px;line-height:1.4}

/* frame chips, photographed corners (img/frame-*.png). "No frame" is the first
   chip rather than a separate framed-or-not toggle. */
.buy-frames{display:grid;grid-template-columns:repeat(5,1fr);gap:6px}
.buy-fr{border:1px solid var(--line);border-radius:8px;padding:6px 3px 5px;background:var(--panel);
  cursor:pointer;text-align:center}
.buy-fr:hover{border-color:var(--accent-line)}
.buy-fr.sel{border-color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent)}
.buy-fr img{width:32px;height:32px;border-radius:5px;display:block;margin:0 auto 4px;object-fit:cover}
.buy-fr .none{width:32px;height:32px;border-radius:5px;margin:0 auto 4px;border:1px dashed var(--line-2);
  display:flex;align-items:center;justify-content:center;color:var(--faint);font-size:9px}
.buy-fr span{font-size:10px;color:var(--dim);display:block;line-height:1.2}
.buy-fr.sel span{color:var(--ink);font-weight:600}

.buy-facts{list-style:none;font-size:11px;color:var(--dim);line-height:1.75;
  margin-top:12px}
.buy-facts li::before{content:"\2713";color:var(--accent);font-weight:700;margin-right:6px}
.buy-facts li.link::before{content:none}
.buy-facts li.link{margin-top:5px}
.buy-facts a{color:var(--accent)}
.buy-fairuse{margin-top:12px;padding-top:10px;border-top:1px solid var(--line-2);
  font-size:11px;line-height:1.5;color:var(--dim)}
.buy-fairuse p{margin:0 0 5px}
.buy-fairuse p:last-child{margin-bottom:0}
.buy-fairuse strong{color:var(--ink);font-weight:600}
.buy-fairuse a{color:var(--accent)}

.buy-total{font-size:12px;color:var(--dim);align-self:center}
.buy-total b{font-size:19px;color:var(--accent);margin-right:6px}
.buy-ship{font-size:11.5px}
#buy-scrim .dlg-foot{justify-content:space-between}
.buy-foot-left{position:relative;display:flex;align-items:center;gap:6px}
/* Map-data credit button now lives in the dialog head, just left of the close
   button; the popover hangs down from it, right-aligned so it stays on-dialog. */
.buy-attrib-wrap{position:relative;display:inline-flex}
.buy-attrib-btn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:none;border-radius:6px;background:none;color:var(--faint);cursor:pointer}
.buy-attrib-btn:hover{background:var(--hover);color:var(--dim)}
.buy-attrib-pop{position:absolute;right:0;top:calc(100% + 8px);z-index:5;width:250px;padding:9px 11px;font-size:11px;line-height:1.5;color:var(--dim);background:var(--panel);border:1px solid var(--line-2);border-radius:8px;box-shadow:0 6px 22px rgba(0,0,0,.16)}
@media (max-width:700px){
  .buy-body{grid-template-columns:1fr}
  .buy-preview{border-right:0;border-bottom:1px solid var(--line-2);padding:16px}
  .buy-frames{grid-template-columns:repeat(5,1fr)}
}

/* the order confirmation's summary table (js/printbuy.js showOrder) */
.printbuy-summary{border:1px solid var(--line-2);border-radius:8px;overflow:hidden;margin:10px 0}
.printbuy-row{display:flex;justify-content:space-between;gap:12px;padding:9px 12px;font-size:12.5px;
  color:var(--dim);border-bottom:1px solid var(--line-2)}
.printbuy-row:last-child{border-bottom:0}
.printbuy-row span:last-child{color:var(--ink);text-align:right}
.printbuy-plain{font-size:12.5px;line-height:1.55;color:var(--dim);margin:0 0 8px}
.printbuy-plain strong{color:var(--ink)}
.printbuy-plain.ok{color:var(--dim)}

/* ---- My orders (js/orders.js) ---- */
#orders-scrim .dialog{width:520px;max-width:94vw}
.orderslist{max-height:52vh;overflow:auto;margin-top:4px}
#printorder-scrim .dlg-foot{justify-content:space-between}

/* order cards, shared by the dialog above */
.ordercard{border:1px solid var(--line-2);border-radius:8px;padding:11px 12px;margin-bottom:8px;
  background:var(--panel)}
.ordercard .orow{display:flex;justify-content:space-between;gap:10px;align-items:baseline}
.ordercard .onum{font-size:12px;font-weight:700;letter-spacing:.03em;color:var(--ink)}
.ordercard .ostatus{font-size:11px;color:var(--dim);text-align:right}
.ordercard .ostatus.good{color:var(--accent)}
.ordercard .ostatus.bad,.ordercard .ometa.bad{color:#c0392b}
.ordercard .ostatus.off{color:var(--faint)}
.ordercard .odesc{font-size:13px;color:var(--ink);margin-top:4px}
.ordercard .ometa{font-size:11.5px;color:var(--faint);margin-top:2px}
.ordercard .oacts{display:flex;gap:6px;flex-wrap:wrap;margin-top:9px}
.ordercard .oacts .mini-btn{font-size:11.5px;padding:4px 9px}
@media (max-width:640px){
  .printbuy-kinds,  }

/* Map-mode chrome that has no business on a poster: the place-search /
   measure / detail toolbar, the dev setup note, and MapLibre's own scale
   control (which would otherwise be composited INTO the printed artwork,
   since the live map is mounted inside the poster). Hidden by CSS rather
   than by JS so there's no show/hide bookkeeping to get wrong on mode
   switches. The support banner deliberately stays — it's monetization
   chrome, not map chrome, and it sits outside the poster. */
body.print-mode .cornerbar,
body.print-mode #setup-note,
body.print-mode .maplibregl-ctrl-bottom-left{display:none}
/* Print keeps the place SEARCH (Aaron, 2026-08-24 — "quickly search a place
   and move the map there" while framing a poster) but none of the rest of the
   map toolbar: measure, level-of-detail and basemap are map-editing tools, and
   the poster's own panel owns detail. */
body.print-mode .maptools > *:not(#searchbox){display:none}
/* Expanded by default (Aaron, 2026-09-27: it used to open only on hover and
   collapse the instant the pointer left, easy to miss it was even there) —
   js/search.js only adds .collapsed once the user is done: picked a result,
   or explicitly closed it (clear button / Escape / click elsewhere). While
   .collapsed IS on, hover/focus/.has-text still reopen it, same as before. */
/* gap:8px from .maptools-panel would push the icon off-centre while the
   (zero-width) input and the clear button are still flex siblings, so the
   collapsed state zeroes it. */
body.print-mode #searchbox{width:260px;padding:0 4px 0 8px;gap:8px;justify-content:flex-start;overflow:visible;
  transition:width .16s ease;cursor:text}
body.print-mode #searchbox.collapsed{width:34px;padding:0;gap:0;justify-content:center;overflow:hidden}
body.print-mode #searchbox .search-ic{display:flex;flex:none}
/* the clear button is a flex sibling too — it must not take space while
   collapsed, or the icon sits left of centre */
body.print-mode #searchbox.collapsed:not(:hover):not(:focus-within):not(.has-text) .search-clear{display:none}
/* The close button only ever shows empty (js/search.js already hides it via
   [hidden] the moment there's a query), and only once the bar is actually
   expanded enough to hold a second control — same exclusion the clear
   button's own rule just above uses for the idle-collapsed icon. */
body.print-mode #searchbox:not(.collapsed) .search-collapse:not([hidden]),
body.print-mode #searchbox.collapsed:hover .search-collapse:not([hidden]),
body.print-mode #searchbox.collapsed:focus-within .search-collapse:not([hidden]){display:grid}
/* flex:none is the load-bearing part. The base rule is `flex:1`, i.e.
   flex-basis:0 + flex-grow:1, and for a flex item the resolved basis beats
   `width` — so the input kept absorbing all the free space (measured 21px of
   a 34px box) and shoved the icon to 6.5px instead of the 17px centre. */
body.print-mode #searchbox.collapsed #search-input{flex:none;min-width:0;width:0;padding:0;opacity:0;transition:opacity .12s ease}
body.print-mode #searchbox.collapsed:hover,
body.print-mode #searchbox.collapsed:focus-within,
body.print-mode #searchbox.collapsed.has-text{width:260px;padding:0 4px 0 8px;gap:8px;justify-content:flex-start}
body.print-mode #searchbox.collapsed:hover #search-input,
body.print-mode #searchbox.collapsed:focus-within #search-input,
body.print-mode #searchbox.collapsed.has-text #search-input{width:auto;flex:1 1 auto;opacity:1}
/* the results list must not be clipped by the collapsed box's overflow */
body.print-mode #searchbox.collapsed:hover,
body.print-mode #searchbox.collapsed:focus-within,
body.print-mode #searchbox.collapsed.has-text{overflow:visible}

/* The close button's "actually collapse it" override (js/search.js). A tap
   on the button — a DESCENDANT of #searchbox — can leave the browser
   insisting #searchbox itself still matches :hover with no pointer to move
   away and clear it, and that pseudo-class doesn't just widen the box, it
   also reopens #search-input and re-shows a button — so overriding width
   alone left the inside still laid out for the wide state, squeezed into a
   34px box. !important on every property those stuck states touch is what
   actually wins regardless of which one the browser insists still matches. */
body.print-mode #searchbox.force-collapsed{width:34px!important;padding:0!important;gap:0!important;justify-content:center!important;overflow:hidden!important;flex:none!important}
body.print-mode #searchbox.force-collapsed #search-input{width:0!important;flex:none!important;opacity:0!important}
body.print-mode #searchbox.force-collapsed .search-collapse,
body.print-mode #searchbox.force-collapsed .search-clear{display:none!important}

/* Focus mode (Adjust Map View / Edit Map Crop) — deliberately the same
   collapse as body.export-mode: rail, panels and every nav item except the
   logo and the active step's bar. Kept as its own class rather than reusing
   export-mode so the two can't interfere if the export flow is ever opened
   from Print. */
body.print-focus .rail,
body.print-focus .left,
body.print-focus .right{display:none}
body.print-focus .canvas{left:0;right:0}
body.print-focus .top > *:not(.logo):not(.exportnav){display:none}
/* Dims the stage — the paper, the panels' former space, everything but the
   map being edited, which sits above this. Same read as the export flow's
   own #export-box shadow, and the nav is outside .canvas so it stays lit. */
/* A CHILD of #poster (map.html), not a stage-level sibling of it, so it
   compares against .postergroup (z-index 16) within #poster's own local
   stacking order — that holds even once the canvas zoom/pan puts a
   `transform` on #poster (js/printdesign.js applyZoom), which otherwise
   turns #poster into its own stacking context and strands a sibling's
   z-index inside it, unable to out-rank .postergroup at all (Aaron: "we
   should not dim the map itself"). The huge negative inset reaches back out
   past #poster's own box to cover the stage around it too — #poster is
   overflow:visible, so it isn't clipped there. */
.posterdim{position:absolute;inset:-100vmax;background:rgba(20,30,45,.45);z-index:14;display:none;pointer-events:none}
body.print-focus .posterdim{display:block}
/* !important: applyStacking() (js/printdesign.js) sets an INLINE z-index on
   this same element for paint order among poster elements (map included,
   often 10 — below the dim's 14), and an inline style otherwise always
   beats a stylesheet rule regardless of this rule's own specificity. Without
   it the map silently went back to being dimmed (Aaron: "we should not dim
   the map itself" — round two, 2026-08-26). */
body.print-focus .postergroup{z-index:16!important}
body.print-focus .supportbanner,
body.print-focus .cornerbar{display:none}

/* Add-element toolbar — same component as .overlaybar (the Overlay tab's
   own docked toolbar); only the contents differ. */
.designbar{position:absolute;left:50%;bottom:16px;transform:translateX(-50%);display:flex;align-items:center;gap:1px;
  background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:6px;
  box-shadow:0 8px 24px rgba(28,44,60,.18);z-index:26}
.designbar[hidden]{display:none} /* display:flex above would beat the hidden attr */
.designbar button{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;width:54px;height:50px;
  border:none;background:none;border-radius:8px;color:var(--dim);cursor:pointer;font-size:9.5px;font-family:inherit;line-height:1}
.designbar button:hover{background:var(--hover);color:var(--ink)}
body.print-focus .designbar{display:none} /* focus modes own the canvas */

/* ---- screen-anchored poster elements (js/printdesign.js) ---- */
.pel{position:absolute;z-index:12;cursor:move}
.pel.sel{outline:1.5px dashed var(--accent);outline-offset:0}
/* A polyline's box is derived from its points, so outlining it draws a
   rectangle the user never made. Its own nodes are the selection cue. */
.pel-t-line.sel{outline:none}
.pel .resizehandle,.pel .sidehandle,.pel .rothandle{display:none}
/* Selected items' handles are drawn by an .hproxy above every element
   (js/printdesign.js syncHandleProxies); the real ones stay in place, hidden,
   and receive the replayed presses. */
.hproxy{position:absolute;z-index:1000;pointer-events:none}
.hproxy > *{pointer-events:auto}
.pel.sel .resizehandle,.pel.sel .sidehandle,.pel.sel .rothandle{display:block}
/* Long rounded bars centred on each edge — drag one and only that axis
   changes. Same idea as the corner discs: they read as a different kind of
   grip, so it's obvious which one scales proportionally and which doesn't. */
.sidehandle{position:absolute;background:var(--panel);border:2px solid var(--accent);border-radius:999px;z-index:6}
.sidehandle[data-side=n],.sidehandle[data-side=s]{left:50%;width:22px;height:8px;margin-left:-11px;cursor:ns-resize}
.sidehandle[data-side=e],.sidehandle[data-side=w]{top:50%;width:8px;height:22px;margin-top:-11px;cursor:ew-resize}
/* -8px = the outline's own 4px offset, plus half the bar's 8px thickness,
   so the bar is centred on the dashed line rather than tangent to it. */
/* -4.75px = -(0.75 + 8/2): same line, for an 8px-thick bar. */
.sidehandle[data-side=n]{top:-4.75px}
.sidehandle[data-side=s]{bottom:-4.75px}
.sidehandle[data-side=e]{right:-4.75px}
.sidehandle[data-side=w]{left:-4.75px}
/* Rotate sits above the top edge, same place js/overlay.js puts an overlay
   element's own rotate grip. */
.rothandle{position:absolute;left:50%;top:-26px;margin-left:-9px;width:18px;height:18px;
  color:var(--accent);cursor:grab;display:grid;place-items:center;z-index:5} /* under the edge/corner handles (6/7) so its grab area never steals theirs */
.rothandle:active{cursor:grabbing}
.rothandle .ico{width:16px;height:16px;stroke-width:2.2}

/* A photo crops like the map element: the image always fills its box, so a
   side drag re-frames rather than squashing. Shapes mirror the map's own. */
/* The crop clips this inner layer, NOT .pel-t-photo itself: clipping the
   wrapper also clipped its handles, so a photo appeared to have none and its
   side bars were sliced in half. */
.pel-cropwrap{position:absolute;inset:0;overflow:hidden}
.pel-cropwrap.crop-circle{border-radius:50%}
.pel-cropwrap.crop-rounded{border-radius:14px}
/* Sibling of .pel-cropwrap, not inside it — the crop's own overflow/border-
   radius would cut a shape-tracing stroke in half at the edge. */
.pel-photo-stroke{position:absolute;inset:0;pointer-events:none}
/* non-circle/rounded shapes get their clip-path inline (js/maskshapes.js
   clipPathFor(), set in js/printdesign.js), not a CSS rule per shape here */
.pel-text{display:flex;align-items:center;line-height:1.25;white-space:pre-wrap;word-break:break-word;overflow:hidden}
/* wrapper type classes are .pel-t-* so they can't collide with the content
   classes above — a wrapper called .pel-text used to inherit the text div's
   own flex/typography rules */
.pel-t-icon,.pel-t-photo,.pel-t-shape{line-height:0}
.pel-shape{width:100%;height:100%}
.pel-photo{width:100%;height:100%;object-fit:cover;display:block;border-radius:2px}
.pel-photo-empty{width:100%;height:100%;display:grid;place-items:center;border:1.5px dashed var(--line-2);
  border-radius:6px;color:var(--faint);background:var(--hover);text-align:center;padding:6px}
/* icon, not the old "Add a photo in the panel" text - too small to read at
   most Photo element sizes and no room at all at small ones (Aaron, 2026-09-28) */
.pel-photo-empty .ico{width:28%;height:28%;min-width:16px;min-height:16px;max-width:48px;max-height:48px}
/* a line's own box is its bounding box; the rule is centred inside it so the
   corner handles land where the element visually is */
.pel-t-line{pointer-events:none} /* the SVG stroke itself is the hit target */
.pel-line{width:100%;height:100%;overflow:visible;display:block}
.pel-line polyline{fill:none;stroke-linecap:round;stroke-linejoin:round;pointer-events:stroke}
.pel-line polyline.hit{stroke:transparent;stroke-width:18;pointer-events:stroke}
.pel-node{position:absolute;width:9px;height:9px;margin:-5px 0 0 -5px;border-radius:50%;
  background:var(--panel);border:2px solid var(--accent);z-index:7;pointer-events:auto;cursor:move}
/* while a polyline is being drawn the whole poster is the drawing surface */
body.print-drawing .poster{cursor:crosshair}
/* Everything on the paper is inert while a polyline is being drawn — the
   map group included. Without it, clicking a point over the map started a
   map drag and stole the selection mid-draw. */
body.print-drawing .pel,
body.print-drawing .postergroup{pointer-events:none}
.pel-icon svg{width:100%;height:100%;display:block}

/* icon picker inside the inspector — same grid the overlay marker picker uses */
.pelicons{display:grid;grid-template-columns:repeat(6,1fr);gap:4px;max-height:180px;overflow-y:auto;
  border:1px solid var(--line-2);border-radius:7px;padding:6px;margin-bottom:9px}
.pelicons button{aspect-ratio:1;border:1px solid transparent;border-radius:6px;background:none;cursor:pointer;
  color:var(--dim);display:grid;place-items:center;padding:3px}
.pelicons button:hover{border-color:var(--accent-line);color:var(--accent)}
.pelicons button.on{border-color:var(--accent);background:var(--accent-soft);color:var(--accent)}
.pelicons svg{width:100%;height:100%;fill:currentColor}

/* Drag-select marquee (js/printdesign.js) — same dashed accent box the
   export flow draws for its area selection. */
.pelmarquee{position:absolute;border:1px dashed var(--accent);background:var(--accent-soft);z-index:19;pointer-events:none}
/* a marquee may start outside the paper, so the poster mustn't clip it */
.poster{overflow:visible}
.pelmarquee[hidden]{display:none}

/* "Clip content" toggle (js/printdesign.js): when on, the poster's CONTENT
   (#postercontent: the Map element and every element) clips to the paper
   edge so it previews exactly as it prints. The selection handles, guides
   and marquee sit outside that layer in #poster, so they're never cut off
   at the edge (Aaron, 2026-09-30: "the handles are cut off when the clip
   content is on"); clipping #poster itself used to take them with it.
   Suspended in focus modes (the dim needs to reach past the paper) and
   while drawing a polyline (which may run off the edge). */
.postercontent{position:absolute;inset:0;border-radius:inherit}
body.print-clip .postercontent{overflow:hidden}
body.print-focus.print-clip .postercontent,
body.print-drawing.print-clip .postercontent{overflow:visible}
.posterclip{position:absolute;top:14px;right:14px;z-index:13;display:flex;align-items:center;gap:6px;
  padding:7px 11px;border:1px solid var(--line-2);border-radius:8px;background:var(--panel);
  color:var(--dim);font-size:12px;font-weight:600;cursor:pointer}
.posterclip:hover{background:var(--hover);color:var(--ink)}
.posterclip.on{background:var(--accent-soft);color:var(--accent);border-color:var(--accent-line)}
.posterclip .ico{width:15px;height:15px}
.posterclip[hidden]{display:none}
body.print-focus .posterclip{display:none}

/* Text is edited in place on the poster rather than through a field in the
   panel, so the element itself is the input. */
.pel-text[contenteditable=true]{outline:none;cursor:text}

/* An element dragged entirely off the paper stays editable but won't be in
   the download, so it reads as set aside rather than deleted. */
.pel.offpaper{opacity:.45}
.pel.offpaper::after{content:'';position:absolute;inset:0;border:1px dashed var(--faint);pointer-events:none}
.offpaper-tag{font-style:normal;font-size:10px;color:var(--faint);margin-left:4px}

/* A design card with no usable thumbnail still needs a picture-shaped block,
   so the list doesn't jump between rows that have one and rows that don't. */
.sd-blank{display:block;width:100%;aspect-ratio:4/5;background:var(--hover)}

/* Print check: what the map will look like as ink at this size. Three
   states, coloured like the app's other status surfaces. */
.printcheck{margin-top:12px;padding:9px 10px;border-radius:7px;border:1px solid var(--line-2);background:var(--hover)}
.printcheck.warn{border-color:#e5a50a;background:rgba(229,165,10,.08)}
.printcheck.error{border-color:#e5a50a;background:rgba(229,165,10,.12)}
.pc-head{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--dim);margin-bottom:3px}
.printcheck.warn .pc-head{color:#a06a00}
.printcheck.error .pc-head{color:#a06a00}
.pc-stat{font-size:11px;color:var(--dim);font-variant-numeric:tabular-nums}
.pc-issue{font-size:11.5px;line-height:1.45;margin-top:6px;color:var(--ink)}

/* Font picker: a search field over a scrolling list, each row previewing in
   its own face. Same shape as the icon picker above. */
.fontpickwrap{position:relative;flex:1;min-width:0}
.fontpick{position:absolute;left:0;right:0;top:34px;max-height:220px;overflow-y:auto;z-index:30;
  background:var(--panel);border:1px solid var(--line-2);border-radius:7px;padding:4px;
  box-shadow:0 8px 24px rgba(28,44,60,.18)}
.fontpick[hidden]{display:none}
.fontrow{display:block;width:100%;text-align:left;border:none;background:none;border-radius:5px;
  padding:6px 8px;font-size:14px;cursor:pointer;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.fontrow:hover{background:var(--hover)}
.fontrow.on{background:var(--accent-soft);color:var(--accent)}

/* Rubber-band preview while drawing a polyline. overflow:visible so a
   segment heading off the paper still draws. */
.drawrubber{position:absolute;inset:0;overflow:visible;z-index:21;pointer-events:none}
.drawrubber[hidden]{display:none}
.drawrubber line{stroke:var(--accent);stroke-width:1.5;stroke-dasharray:4 4}
.drawrubber circle{fill:var(--panel);stroke:var(--accent);stroke-width:2}

/* Centre guides. Magenta rather than the theme accent on purpose: the
   accent is already the selection colour here, and an alignment cue has to
   read as a different kind of thing from the box it's aligning. */
.posterguide{position:absolute;background:#e5399b;z-index:20;pointer-events:none}
.posterguide[hidden]{display:none}
.posterguide.vert{top:0;bottom:0;width:1px}
.posterguide.horz{left:0;right:0;height:1px}

/* Reorder feedback in the Customize list, reusing the Overlay list's own
   .dragging/.drop-before/.drop-after rules above. */
#design-list .ovrow{cursor:grab}
#design-list .ovrow.dragging{cursor:grabbing}

/* Pan mode: the surface itself is the drag target, so it takes the cursor
   and nothing underneath may claim the press. */
body.print-pan .posterstage,
body.print-pan .poster,
body.print-pan .pel{cursor:grab !important}
body.print-pan .posterstage:active,
body.print-pan .poster:active{cursor:grabbing !important}
body.print-pan .pel,
body.print-pan .postergroup{pointer-events:none}
.designbar button.on{background:var(--accent-soft);color:var(--accent)}
/* .obsep is defined for .overlaybar only, so the divider needs its own rule
   here to actually separate Move/Pan from the add tools. */
.designbar .obsep{width:1px;height:30px;background:var(--line);margin:0 3px;flex:none}

/* ---- the poster stage (js/printdesign.js) ---- */
/* overflow:hidden, not auto: panning/zooming is done entirely by the
   transform on #poster (js/printdesign.js applyZoom), never by scrolling —
   overflow:auto let the browser grow real scrollbars once the zoomed poster
   exceeded the stage, and scrolling that container shifted where the
   absolutely-positioned .designbar (bottom:16px of THIS box) painted, so the
   toolbar visibly jumped while panning (Aaron). Content past the stage edge
   is meant to be clipped, not scrolled to. */
.posterstage{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;overflow:hidden;padding:var(--poster-pad,40px);z-index:12}
.posterstage[hidden]{display:none} /* display:flex above would beat the hidden attr */
/* Opening straight into Print: a loading cover over everything below the top
   bar (and the Map-mode buttons in it) until js/printmode.js bootFromUrl()
   actually enters Print, instead of showing the Map editor first */
.print-boot-cover{display:none}
body.print-boot .print-boot-cover{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;position:fixed;
  top:var(--topH);left:0;right:0;bottom:0;z-index:146;background:var(--canvas);color:var(--dim);font-size:13px}
body.print-boot .print-boot-cover .spin{width:28px;height:28px;border:3px solid var(--line);border-top-color:var(--accent);border-radius:50%;animation:tpl-spin .8s linear infinite}
body.print-boot .top > *:not(.logo){visibility:hidden}
/* A new poster waiting on its first template (js/printtemplates.js
   setWaiting): a loading state instead of the blank default sheet */
body.print-mode.tpl-loading #poster{visibility:hidden}
body.print-mode.tpl-loading .posterstage::before{content:"";position:absolute;left:50%;top:50%;width:28px;height:28px;margin:-34px 0 0 -14px;
  border:3px solid var(--line);border-top-color:var(--accent);border-radius:50%;animation:tpl-spin .8s linear infinite}
body.print-mode.tpl-loading .posterstage::after{content:"Loading template…";position:absolute;left:0;right:0;top:50%;margin-top:8px;
  text-align:center;font-size:13px;color:var(--dim)}
@keyframes tpl-spin{to{transform:rotate(360deg)}}
/* Templates gallery placeholders until the first snapshot arrives */
.presetcard.tpl-skel{cursor:default;pointer-events:none}
.tpl-skel-img{display:block;aspect-ratio:4/5;background:var(--hover);animation:tpl-pulse 1.2s ease-in-out infinite}
.tpl-skel-line{display:block;width:60%;height:10px;border-radius:5px;background:var(--hover);animation:tpl-pulse 1.2s ease-in-out infinite}
/* a real card whose thumbnail is still on its way (js/printtemplates.js thumbEl) */
.presetcard .tpl-thumb-wait{aspect-ratio:4/5;background:var(--hover);animation:tpl-pulse 1.2s ease-in-out infinite}
.tpl-retry{grid-column:1/-1;width:100%;height:40px}
@keyframes tpl-pulse{50%{opacity:.5}}
.poster{position:relative;background:#fff;border-radius:4px;box-shadow:0 14px 44px rgba(20,30,45,.24);flex:none;user-select:none}

/* The Map element. Its box is set in px by JS so drag math stays plain
   arithmetic in the poster's own coordinate space. */
.postergroup{position:absolute}
/* 0 offset, matching .pel.sel — the handle offsets below are computed against one
   outline distance, and the map having its own 6px is what left its handles
   sitting ~2.75px inside the dashed line (Aaron, 2026-08-25). */
.postergroup.sel .maskwin,
.postergroup.mode-mask .maskwin{outline:1.5px dashed var(--accent);outline-offset:0}
.postergroup.mode-group{cursor:move}
/* The mask window itself. Defaults to a plain box (§9.3); #map is relocated
   in here and fills it via its own inset:0. */
.maskwin{position:absolute;overflow:hidden;border-radius:2px}
/* Traces the crop shape (js/printdesign.js renderMapStroke). Positioned onto
   the map box by the same code that positions .maskwin; overflow visible so a
   stroke straddling the edge shows both halves. */
.maskstroke{position:absolute;overflow:visible;pointer-events:none}
.maskwin.mask-circle{border-radius:50%}
.maskwin.mask-rounded{border-radius:14px}
/* non-circle/rounded shapes get their clip-path inline (js/printdesign.js
   render(), via js/maskshapes.js clipPathFor()), not a CSS rule per shape */
.postergroup.mode-mask .maskwin{cursor:move;outline:2px solid var(--accent)}
.postergroup.mode-pan .maskwin{cursor:grab}
.postergroup.mode-pan .maskwin:active{cursor:grabbing}

.draglayer{position:absolute;inset:0;z-index:5}
.postergroup.mode-pan .draglayer{pointer-events:none} /* pan hands the map back its own drag */
/* Selection handles, deliberately matching what js/overlay.js draws on the
   map for every other element rather than inventing a second visual
   language for the poster: a white disc with a 2px accent ring (its
   circle-radius 5.5 / circle-stroke-width 2 spec, in CSS px), four of them,
   one per corner, sitting just outside the dashed box. */
.resizehandle{position:absolute;width:11px;height:11px;background:var(--panel);border:2px solid var(--accent);
  border-radius:50%;z-index:7;display:none} /* 7: corners win over the side bars (6) where they meet on a small element */
/* -6.25px = -(0.75 + 11/2): centres an 11px disc on the dashed line. */
.resizehandle[data-corner=nw]{left:-6.25px;top:-6.25px;cursor:nwse-resize}
.resizehandle[data-corner=ne]{right:-6.25px;top:-6.25px;cursor:nesw-resize}
.resizehandle[data-corner=se]{right:-6.25px;bottom:-6.25px;cursor:nwse-resize}
.resizehandle[data-corner=sw]{left:-6.25px;bottom:-6.25px;cursor:nesw-resize}
/* Shown whenever the element is selected OR being cropped — same handles,
   different job (js/printdesign.js branches on the mode). Hidden in Adjust
   Map View, where the drag belongs to the map camera, not the box. */
.postergroup .sidehandle{display:none}
.postergroup.sel.mode-group .resizehandle,
.postergroup.sel.mode-group .sidehandle,
.postergroup.mode-mask .resizehandle{display:block}
/* circle/heart/star force a 1:1 crop (forcedAspect) — a side handle only
   scales one axis, which either no-ops or fights the lock, so it's dropped
   for these shapes rather than shown and ignored (Aaron). Wins over the
   .sel.mode-group rule above via source order + equal specificity. */
.postergroup.no-sidehandles .sidehandle{display:none}


/* 5: label and control share one line, exactly like the Overlay inspector
   (Aaron, 2026-08-24 — reversing the stacked layout from 08-23). Inherits the
   base .prop flex row; only the label COLUMN is widened, because Print's
   labels ("Alignment", "Content", "Detail level") don't fit the 44px gutter
   Overlay's short ones do. */
.printinsp .prop .plabel{width:74px;flex:none}
.printinsp .prop .colorfield,
.printinsp .prop .proptext,
.printinsp .prop .fontpickwrap{flex:1;width:auto;min-width:0}
/* .proptext is flex:1, but .fontpickwrap is position:relative and NOT a flex
   container, so the input fell back to an <input>'s intrinsic size and spilled
   past the panel once the label moved beside it. */
.printinsp .prop .fontpickwrap .proptext{width:100%}
.printinsp .prop .rangefield{flex:1;min-width:0}
.printinsp .ovtogglegroup{display:flex;gap:6px;flex:1;min-width:0}
/* .prop-stack (js/printdesign.js photoFitRow's "Photo fill"): label on its
   own line, input full width below — same label-above/input-below shape
   .settingsbody's own .prop rows already use elsewhere, just scoped to one
   row instead of a whole panel, for a value too long to share the label's
   line comfortably even after the overflow fix (min-width:0 + ellipsis on
   .prop select.dlg-input) left it readable but cramped. */
.printinsp .prop.prop-stack{display:block}
.printinsp .prop.prop-stack .plabel{display:block;width:auto;margin-bottom:5px}
.printinsp .prop.prop-stack select.dlg-input{width:100%;flex:none}

/* 6: the map's Render sliders keep a label/value row — three sliders read
   better aligned than stacked — with fixed columns so the numbers line up. */
#pd-render .prop{display:flex;align-items:center;margin-bottom:9px}
#pd-render .prop .plabel{display:block;width:60px;flex:none;margin-bottom:0}
#pd-render .prop .rangefield{flex:1;min-width:0}
#pd-render .prop .rv{width:30px;flex:none;text-align:right}

/* Map element inspector (right panel). Padding and inter-field rhythm copied
   from the Overlay inspector (.prop's 9px) so the two panels feel like one
   component rather than two that nearly match. */
.printinsp{padding:12px 12px 16px}
/* Section labels double as dividers, so the panel reads as grouped
   properties rather than one long column (Aaron, 2026-08-23). The first one
   gets no rule or top gap — that space above the title was dead. */
.printinsp .fieldlabel{margin-top:14px;padding-top:12px;border-top:1px solid var(--line);display:flex;align-items:baseline;justify-content:space-between}
.printinsp > .fieldlabel:first-child{margin-top:0;padding-top:0;border-top:none}
.fieldlabel .cnt{font-weight:600;letter-spacing:0;text-transform:none;color:var(--faint)}
.printinsp .hint{margin-top:12px}
/* the inspector head carries the title alone here (no crumb), so its top
   padding is the crumb's old space — take it back */
body.print-mode .insp-head{padding-top:10px}
body.print-mode #insp-crumb{display:none}
/* Holds .msp-open (thumb/name/chevron) — opens the saved-map picker
   (js/printstyle.js #mapstyle-scrim). The picker itself now carries a
   pinned "Default map" card for reverting (2026-08-27), so this row no
   longer has a second "x" remove control beside it. */
.mapstylepreview{display:flex;align-items:center;gap:6px;width:100%;padding:8px;border:1px solid var(--line-2);border-radius:8px;background:var(--panel)}
.msp-open{display:flex;align-items:center;gap:10px;flex:1;min-width:0;border:none;background:none;padding:0;margin:0;font:inherit;color:inherit;text-align:left;cursor:pointer}
.msp-open:hover{color:var(--accent)}
.mapstylepreview:has(.msp-open:hover){border-color:var(--accent-line);background:var(--hover)}
.msp-thumb{width:40px;height:40px;border-radius:6px;flex:none;background:var(--hover);object-fit:cover;display:block;overflow:hidden}
/* The picture handed over is markup built for the MODAL CARD (.msthumb,
   width:100%), so dropped into this 40px slot it painted at full panel width
   and covered everything under it (Aaron, 2026-08-25). Constrain whatever
   lands here rather than making the caller pre-size it for one consumer. */
.msp-thumb > *{width:100%;height:100%;display:block;object-fit:cover;aspect-ratio:auto}
.msp-text{flex:1;min-width:0}
.msp-name{display:block;font-size:12.5px;font-weight:650;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.msp-sub{display:block;font-size:10.5px;color:var(--faint)}
.msp-chev{color:var(--faint);flex:none;display:flex}
/* The map object's two actions, one row (2026-08-27 — replaces two stacked
   .inspbtn full-width buttons). Sits right under .mapstylepreview so the
   source row + these two read as one grouped "Map" object, no container. */
.actrow{display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-top:8px}
/* A lone action (js/printdesign.js movePhotoRow's "Move photo") — same row
   spacing as the two-button .actrow, one column instead of two. */
.actrow.single{grid-template-columns:1fr}
.actbtn{display:flex;align-items:center;justify-content:center;gap:6px;height:32px;border:1px solid var(--line-2);border-radius:7px;background:var(--panel);color:var(--ink);font:inherit;font-size:12px;font-weight:600;cursor:pointer}
.actbtn:hover{background:var(--hover);border-color:var(--dim)}
.actbtn span[data-icon]{color:var(--dim)}
.actbtn.on{background:var(--accent-soft);color:var(--accent);border-color:var(--accent-line)}
.actbtn.on span[data-icon]{color:var(--accent)}
.actbtn:disabled{opacity:.5;cursor:default}
/* Style: an inline, browsable swatch grid (2026-08-27 — replaces a row that
   opened a separate modal, js/printpreset.js). Capped so Render/Crop/Stroke
   below stay reachable without scrolling the whole panel; internal scroll
   for the rest. 3 columns × the mock's own cap (design/
   printmapstyle-option-a-compact.html, built against this same real
   css/app.css panel width, so the value carries over unchanged). */
.stygrid{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin-top:4px}
.stygrid.capped{max-height:174px;overflow-y:auto;padding:2px;margin:2px -2px 0}
.stysw{position:relative;aspect-ratio:1;border:1.5px solid var(--line-2);border-radius:7px;overflow:hidden;cursor:pointer;padding:0;background:var(--hover)}
.stysw .pp-map{width:100%;height:100%;object-fit:cover;display:block}
.stysw .nm{position:absolute;left:0;right:0;bottom:0;z-index:1;background:rgba(255,255,255,.92);font:650 9px/1.1 var(--font);color:#1e2328;padding:3px 4px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.stysw .ck{position:absolute;top:3px;right:3px;z-index:2;width:15px;height:15px;border-radius:50%;background:var(--accent);color:#fff;display:none;place-items:center}
.stysw .ck svg,.stysw .ck span{width:10px;height:10px;display:block}
.stysw.on{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-soft)}
.stysw.on .ck{display:grid}
.maskpicker{display:flex;gap:8px;margin-bottom:10px;flex-wrap:wrap}
.maskbtn{width:40px;height:40px;border-radius:8px;border:1.5px solid var(--line-2);background:var(--panel);display:grid;place-items:center;cursor:pointer;color:var(--dim)}
.maskbtn.on{border-color:var(--accent);background:var(--accent-soft);color:var(--accent)}
.maskbtn .swatch{width:18px;height:18px;background:currentColor}
.maskbtn .swatch.mask-circle{border-radius:50%}
.maskbtn .swatch.mask-rounded{border-radius:5px}
/* non-circle/rounded shapes get their clip-path inline (js/printdesign.js
   maskBtn(), via js/maskshapes.js clipPathFor()), not a CSS rule per shape */

/* Map style modal (js/printstyle.js). Reuses .dialog.wide + .codetabs, the
   same two-tab shell the Publish dialog already uses. */
.mapstyledlg{max-height:82vh;display:flex;flex-direction:column}
.dlg-head-actions{display:flex;align-items:center;gap:8px}
.msdlg-body{overflow-y:auto;padding:16px;flex:1;min-height:0}
.msgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:14px}
.msgrid[hidden]{display:none} /* display:grid above would beat the hidden attr */
.mscard{position:relative;border:1.5px solid var(--line);border-radius:9px;overflow:hidden;background:var(--panel)}
.mscard.sel{border-color:var(--accent)}
.mscard .msthumb{display:block;width:100%;aspect-ratio:1;object-fit:cover;background:var(--hover)}
.mscard .msthumb svg{width:100%;height:100%;display:block}
.mscard .msname{display:block;padding:7px 9px;font-size:12px;font-weight:600;border-top:1px solid var(--line);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* The per-card action only appears on hover/focus, so the grid reads as a
   gallery at rest instead of a wall of buttons (§9.4). */
.msactions{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
  background:rgba(20,30,45,.42);opacity:0;transition:opacity .12s}
.mscard:hover .msactions,.mscard:focus-within .msactions{opacity:1}
/* Delete, same reveal-on-hover as .msactions but its own small corner button
   (Aaron: this was missing from the picker entirely — "hidden" in the sense
   of never shown, not clipped) so it reads as a distinct destructive action
   rather than competing with "Use this map" for one click target. Painted
   after .msactions in the DOM, so it sits above that overlay with no
   z-index needed. */
.msdelete{position:absolute;top:6px;right:6px;width:26px;height:26px;border:none;border-radius:6px;
  background:rgba(20,30,45,.6);color:#fff;display:flex;align-items:center;justify-content:center;
  cursor:pointer;opacity:0;transition:opacity .12s}
.mscard:hover .msdelete,.mscard:focus-within .msdelete{opacity:1}
.msdelete:hover:not(:disabled){background:#d33}
.msdelete:disabled{cursor:default;opacity:.3}
.msdelete .ico{width:14px;height:14px}

/* Print Settings tab: a direct form, same shape as the Import tab — no list,
   nothing to select, just fields. */
/* Two row layouts, and which one a panel uses depends on whether it FITS
   (Aaron, 2026-08-24): the right-hand property panels are one line
   (label | control, see .printinsp), but the Layout tab stacks, because its
   labels and its inputs are both long enough that one line squeezes the
   control to nothing. */
.settingsbody{padding:12px 12px 16px}
.settingsbody .prop{display:block;margin-bottom:9px}
.settingsbody .prop .plabel{display:block;width:auto;margin-bottom:5px}
.settingsbody .prop .proptext,
.settingsbody .prop select.dlg-input{width:100%;flex:none}
.settingsbody .colorfield{width:100%}
.settingsbody .ovtogglegroup{display:flex;gap:6px}
.segrow{display:flex;gap:6px}
.swatchrow{display:flex;gap:6px}
.colorswatch{width:26px;height:26px;border-radius:50%;border:2px solid var(--panel);box-shadow:0 0 0 1px var(--line-2);cursor:pointer}
.colorswatch.on{box-shadow:0 0 0 2px var(--accent)}

/* ---------- icon rail ---------- */
.rail{position:fixed;top:var(--topH);left:0;bottom:0;width:var(--railW);background:var(--panel);border-right:1px solid var(--line);z-index:36;display:flex;flex-direction:column;padding:8px 0}
.railbtn{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:5px;height:60px;border:none;background:none;color:var(--dim);cursor:pointer}
/* The rail carries both modes' tabs and js/printmode.js toggles the `hidden`
   attribute on them — which display:flex above would otherwise beat, showing
   all nine at once. Same trap as .overlaybar/.ovpz-urlrow elsewhere here. */
.railbtn[hidden]{display:none}
.railbtn:hover{background:var(--hover)}
.railbtn .lbl{font-size:10.5px;letter-spacing:.01em}
.railbtn.active{background:var(--accent-soft);color:var(--accent)}
.railbtn.active .lbl{font-weight:640}

/* ---------- left secondary panel ---------- */
.left{position:fixed;top:var(--topH);left:var(--railW);bottom:0;width:var(--leftW);background:var(--panel);border-right:1px solid var(--line);z-index:35;overflow:hidden}
/* Preset panel "expand" — widens over the canvas (already higher z-index)
   and switches the list to a grid so more cards are visible at once; the
   right inspector is already hidden while on the Preset tab, so nothing
   else needs to move out of the way. */
body.preset-expanded .left{width:calc(100vw - var(--railW))}
body.preset-expanded .presetlist{display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));grid-auto-rows:min-content;gap:14px;align-content:start;align-items:start}
body.preset-expanded .presetcard{width:auto}
/* My maps / My designs "expand" (js/mymaps.js, js/printsaved.js) — same
   widen-over-the-canvas-and-grid treatment as the Preset panel's own expand
   above, just keyed off a separate body class per panel since either can be
   open without the other. */
body.mymaps-expanded .left,body.design-expanded .left{width:calc(100vw - var(--railW))}
body.mymaps-expanded #mymap-list,body.design-expanded #saveddesign-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));grid-auto-rows:min-content;gap:14px;align-content:start;align-items:start}
body.mymaps-expanded #mymap-list .presetcard,body.design-expanded #saveddesign-list .presetcard,
body.mymaps-expanded #mymap-list .foldercard,body.design-expanded #saveddesign-list .foldercard{width:auto}
/* Print mode's My designs/Templates tabs (unlike the Preset tab above) run
   with the right inspector still visible, and .right shares .left's own
   z-index:35 — with equal z-index the later element in the DOM (.right)
   paints on top, so the expanded panel would run UNDER the inspector rather
   than over it. Bump only these two expanded states above .right's 35. */
body.design-expanded .left{z-index:40}
/* Print mode's Templates tab "expand" (js/printtemplates.js) — same treatment
   again; #template-list already shares the Preset panel's .presetlist/
   .presetcard classes, so this is just the Preset block's selectors keyed off
   its own body class. */
body.template-expanded .left{width:calc(100vw - var(--railW));z-index:40}
body.template-expanded .presetlist{display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));grid-auto-rows:min-content;gap:14px;align-content:start;align-items:start}
body.template-expanded .presetcard{width:auto}
.panelbody{height:100%;display:flex;flex-direction:column}
.panelbody[hidden]{display:none}
.lhead{display:flex;align-items:center;justify-content:space-between;height:38px;padding:0 12px;border-bottom:1px solid var(--line);flex:none}
.lhead .ttl{font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--dim)}
/* Row up a panel header's action buttons horizontally. Without this, a
   .headbtn's own display:flex (for centering its icon) blockifies the
   button — so with more than one visible here, .lhead-actions being a plain
   block div stacked them as separate lines instead of side by side. Every
   panel had at most one visible headbtn until My maps/My designs picked up
   Select + Expand alongside "+" (2026-09-13), which is what surfaced it; the
   @media (max-width:760px) block below already set this for mobile, so this
   just extends the same layout to desktop. */
.lhead-actions{display:flex;align-items:center;gap:4px}

/* layers tree */
.tree{flex:1;overflow-y:auto;padding:6px}
.tree.disabled{opacity:.45;pointer-events:none}
.row{display:flex;align-items:center;gap:6px;height:30px;padding:0 8px;border-radius:6px;cursor:pointer;position:relative}
.row:hover{background:var(--hover)}
.row:hover .eye{color:var(--ink)}
.row.sel{background:var(--accent-soft)}
.row.sel .nm{font-weight:600}
.row .chev{width:14px;height:14px;display:grid;place-items:center;color:var(--faint);flex:none}
.row .chev.leaf{visibility:hidden}
.row .sw{width:14px;height:14px;border-radius:4px;flex:none;box-shadow:inset 0 0 0 1px rgba(20,30,45,.18)}
.row .sw.cat{background:none;box-shadow:none;display:grid;place-items:center;color:var(--dim)}
.row .nm{flex:1;font-size:12.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.row .eye{color:var(--dim);display:grid;place-items:center;width:20px;height:20px;border:none;background:none;cursor:pointer}
.row.off .nm{color:var(--faint)}
.row.off .eye{color:var(--faint)}
.indent{width:16px;flex:none}

/* basemap type (Map / Satellite / Terrain) */
.basemaptypes{display:flex;gap:6px;padding:8px;flex:none}
.basemapcard{flex:1;padding:8px 4px;border-radius:8px;cursor:pointer;border:1px solid var(--line);background:none;text-align:center;font-size:12px;font-weight:600;color:var(--dim)}
.basemapcard:hover{background:var(--hover)}
.basemapcard.sel{background:var(--accent-soft);border-color:var(--accent-line);color:var(--accent)}

/* presets — preview tiles; tap applies */
.presetimport{display:flex;flex-direction:column;gap:6px}
.presetimport textarea{border:1px solid var(--line-2);border-radius:6px;padding:7px 9px;font:inherit;font-size:11.5px;outline:none;resize:vertical;min-height:44px;font-family:ui-monospace,Consolas,monospace}
.presetimport textarea:focus{border-color:var(--accent-line)}
.presetlist{flex:1;overflow-y:auto;padding:10px;display:flex;flex-direction:column;gap:10px}
.presetlist.disabled{opacity:.45;pointer-events:none}
/* Shared "this tab is disabled on Satellite/Terrain" banner (Preset + Style
   tabs) — sits at the top of the panel, above the (dimmed) list/tree, so it
   reads as an explanation rather than a caption underneath. */
.disablednote{margin:10px 12px;padding:10px 12px;background:var(--hover);border:1px solid var(--line-2);border-radius:8px;font-size:11.5px;color:var(--dim);line-height:1.45}
/* flex-shrink:0 is the fix here — .presetlist already scrolls
   (overflow-y:auto), but a flex column's items still shrink to fit by
   default even so, so without this the cards themselves got squeezed
   (image + name row clipped) instead of the list just scrolling past them */
.presetcard{position:relative;display:flex;flex-direction:column;flex-shrink:0;width:100%;padding:0;border-radius:9px;cursor:pointer;border:1.5px solid var(--line);background:var(--panel);text-align:left;overflow:hidden}
.presetcard:hover{border-color:var(--accent-line)}
.presetcard.sel{border-color:var(--accent)}
/* Template drag-to-reorder (admin, js/printtemplates.js wireCardDrag). Inset
   shadows, not ::before/::after bars like .ovrow's: .presetcard clips its
   overflow, so a bar on the edge would be cut off. drop-x = the expanded grid,
   where cards sit side by side and the gap is left/right. */
.presetcard.dragging{opacity:.4}
.presetcard.drop-before{box-shadow:inset 0 3px 0 var(--accent)}
.presetcard.drop-after{box-shadow:inset 0 -3px 0 var(--accent)}
.presetcard.drop-x.drop-before{box-shadow:inset 3px 0 0 var(--accent)}
.presetcard.drop-x.drop-after{box-shadow:inset -3px 0 0 var(--accent)}
.presetcard .pp-map{width:100%;display:block;flex:none}
.presetcard .prow{display:flex;align-items:center;gap:6px;padding:6px 9px;border-top:1px solid var(--line);flex:none}
.presetcard .pname{font-size:12px;font-weight:600;line-height:1.3;flex:1;word-break:break-word}
.presetcard .check{color:var(--accent);opacity:0;display:grid;place-items:center}
.presetcard.sel .check{opacity:1}
/* 3-dot row menu — one shared #mymaps-rowmenu (below), not a per-card dropdown */
.presetcard .pmore{color:var(--faint);flex:none;display:grid;place-items:center;width:22px;height:22px;border-radius:5px}
.presetcard .pmore:hover{color:var(--ink);background:var(--hover)}
/* .dropmenu is normally position:absolute inside a .navdrop{position:relative}
   (see #doctitle-drop) — this one is shared across every scrolling row in
   .mymaplist, so it's positioned in fixed viewport coordinates instead
   (js/mymaps.js openRowMenu()), same reasoning as the CSV importer's .mp-menu. */
#mymaps-rowmenu{position:fixed}
#design-rowmenu{position:fixed}
/* Every folder dropdown is fixed for the same reason the row menus are: the
   list under them scrolls and would clip an absolutely-positioned menu. */
#mymaps-addmenu,#design-addmenu,#folder-rowmenu,#folder-movemenu{position:fixed}
#folder-movemenu{max-height:280px;overflow-y:auto;max-width:240px}
#folder-movemenu button span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* Folder row (js/folders.js cardHtml). Deliberately NOT a .presetcard — those
   are big thumbnail tiles, and a stack of identical folder tiles would bury
   the maps underneath them. A slim labelled row reads as a different kind of
   thing at a glance, and folders sort above the cards. */
.foldercard{display:flex;align-items:center;gap:8px;width:100%;flex-shrink:0;padding:9px 10px;
  border:1.5px solid var(--line);border-radius:9px;background:var(--panel);cursor:pointer;text-align:left}
.foldercard:hover{border-color:var(--accent-line);background:var(--hover)}
.foldercard .fc-ic{color:var(--accent);display:grid;place-items:center;flex:none}
.foldercard .fc-name{font-size:12.5px;font-weight:600;flex:1;word-break:break-word}
.foldercard .fc-count{font-size:11px;color:var(--dim);flex:none;font-variant-numeric:tabular-nums}
.foldercard .fc-more{color:var(--faint);flex:none;display:grid;place-items:center;width:22px;height:22px;border-radius:5px}
.foldercard .fc-more:hover{color:var(--ink);background:var(--hover)}

/* Panel title + its back chevron, grouped so .lhead's space-between still
   pushes .lhead-actions hard right. */
.lhead-main{display:flex;align-items:center;gap:2px;min-width:0}
.lhead-main .ttl{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.lhead .crumb-back{flex:none;margin-left:-6px}
.lhead .crumb-back[hidden]{display:none}
#tpl-rowmenu{position:fixed}

/* Saved designs use the same card as saved maps, and the same LANDSCAPE
   thumbnail box (Aaron, 2026-08-24) — a poster is usually portrait, and
   letting the picture set the card's height made each one enormous and put
   two designs on a screen. The poster is centred in the box at its own aspect
   with grey either side, which also reads as "this one is portrait". */
#saveddesign-list .presetcard .pp-map{aspect-ratio:16/10;height:auto;object-fit:contain;background:var(--canvas)}
#saveddesign-list .presetcard .pp-map.sd-blank{display:block}

/* admin preset publishing block */
.admin-preset{flex:none;border-top:1px solid var(--line);padding:10px;display:flex;flex-direction:column;gap:6px}
.admin-preset[hidden]{display:none} /* display:flex would beat the hidden attr */

/* import */
.importbody{padding:14px;display:flex;flex-direction:column;gap:14px;overflow-y:auto;flex:1}
.fieldlabel{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--dim);margin-bottom:6px}
.pasterow{display:flex;gap:6px}
.pasterow input{flex:1;height:32px;border:1px solid var(--line-2);border-radius:6px;padding:0 10px;font:inherit;font-size:12.5px;outline:none;min-width:0}
.mini-btn{height:32px;padding:0 12px;border-radius:6px;border:none;background:var(--accent);color:#fff;font-size:12.5px;font-weight:600;cursor:pointer}
.mini-btn:disabled{background:var(--line-2);cursor:default}
/* An <a class="mini-btn"> (e.g. #maplimit-upgrade) gets none of a <button>'s
   default centered-content rendering — plain inline text just sits at its
   line-height position instead of centering in the fixed height, and picks
   up the browser default underline. */
a.mini-btn{display:inline-flex;align-items:center;justify-content:center;text-decoration:none}
.hint{font-size:11.5px;color:var(--faint);line-height:1.45}
/* "This map is linked to a Google Sheet" (js/sheetsync.js). A compact card
   pinned above the overlay list (not in the Import tab — the link is the
   map's state, so it lives with the map's shapes). Tapping it opens a
   3-action .dropmenu (Open / Sync / Unlink). */
.sheetcard{position:relative;flex:none;margin:8px 8px 0}
.sheetcard[hidden]{display:none}
.sheetcard-btn{display:flex;align-items:center;gap:6px;width:100%;padding:6px 8px;border:1px solid var(--line-2);border-radius:6px;background:var(--hover);cursor:pointer;font:inherit;font-size:11.5px;color:var(--dim)}
.sheetcard-btn:hover{border-color:var(--accent-line)}
.sheetcard-ico{flex:none}
.sheetcard-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:left;color:var(--ink);font-weight:600}
.sheetcard-caret{flex:none;transition:transform .15s}
.sheetcard.open .sheetcard-caret{transform:rotate(180deg)}
/* Anchored under the card, spanning its width, rather than the .dropmenu
   default (top:36px;left:0;min-width:150px). */
.sheetcard .dropmenu{top:calc(100% + 4px);left:0;right:0;min-width:0}
.ordiv{display:flex;align-items:center;gap:8px;color:var(--faint);font-size:11px}
.ordiv:before,.ordiv:after{content:"";flex:1;height:1px;background:var(--line)}
.dropzone{border:1.5px dashed var(--line-2);border-radius:8px;padding:22px 10px;display:flex;flex-direction:column;align-items:center;gap:8px;color:var(--faint);font-size:12px;text-align:center;cursor:pointer}
.dropzone.dragover{border-color:var(--accent);color:var(--accent);background:var(--accent-soft)}

/* my maps — same card look as presets (screenshot + name + actions) */
.mymaplist{flex:1;overflow-y:auto;padding:10px;display:flex;flex-direction:column;gap:10px}
/* Multi-select checkbox, shown over the thumbnail only once something is
   selected (js/mymaps.js, js/printsaved.js) — a circle rather than reusing
   .presetcard's own .check/.sel (that pair already means "this is the
   applied preset" on the Preset tab; a different visual role here). .msel
   itself also gets a border/background so js/folders.js's initDragSelect()
   can preview a live drag by toggling just that class, before the checkbox
   span (added only once the caller re-renders) exists at all. */
.presetcard .pcheck{position:absolute;top:6px;left:6px;width:20px;height:20px;border-radius:50%;
  background:rgba(255,255,255,.92);border:1.5px solid var(--line-2);display:grid;place-items:center;
  color:transparent;z-index:1}
.presetcard.msel{border-color:var(--accent);background:var(--accent-soft)}
.presetcard.msel .pcheck{background:var(--accent);border-color:var(--accent);color:#fff}
/* The marquee itself (js/folders.js initDragSelect) — position:fixed and
   appended to <body> so it tracks the cursor in viewport coordinates
   regardless of the list's own scroll position, same reasoning as the
   dropmenus' own position:fixed. */
.select-marquee{position:fixed;border:1.5px dashed var(--accent);background:var(--accent-soft);
  opacity:.7;pointer-events:none;z-index:150}

/* save/rename dialog input */
.dlg-input{width:100%;height:34px;border:1px solid var(--line-2);border-radius:6px;padding:0 10px;font:inherit;font-size:13px;outline:none}
.dlg-input:focus{border-color:var(--accent-line)}
.dlg-note{margin-top:10px;font-size:12px;line-height:1.5;color:var(--dim);background:var(--accent-soft);border:1px solid var(--accent-line);border-radius:6px;padding:8px 10px}
.dlg-note[hidden]{display:none}

/* delete-map confirm — first destructive confirm in the app; #d33 is the
   shared danger red reused wherever else something destructive needs one */
.mini-btn.danger{background:#d33}
.mini-btn.danger:hover{background:#b82c2c}
/* `ghost danger` is the destructive ORDER action (Cancel order, Refund +
   notify, Cancel this order). It was a mess: .danger above wins the red
   background, but .ghost (equal specificity, declared far earlier) still
   supplies the text and border, so the button rendered red with blue text
   inside a blue outline. These cancel or refund something someone paid real
   money for, so they get the solid red treatment outright. Needs all three
   classes (0,3,0) to beat both single-class rules regardless of order, and
   :disabled has to be restated for the same reason — otherwise it would
   stay red while a refund is in flight. */
.mini-btn.ghost.danger{background:#d33;color:#fff;border-color:#d33}
.mini-btn.ghost.danger:hover{background:#b82c2c;border-color:#b82c2c}
.mini-btn.ghost.danger:disabled{background:var(--line-2);border-color:var(--line-2);color:#fff}
.confirmtext{font-size:13px;line-height:1.55}
.confirmtext .dlg-warn-name{font-weight:650}

/* ---------- canvas / map ---------- */
.canvas{position:fixed;top:var(--topH);left:calc(var(--railW) + var(--leftW));right:var(--rightW);bottom:0;background:var(--canvas);z-index:10}
body.inspector-hidden .canvas{right:0}
body.inspector-floating .canvas{right:0}
body.inspector-floating .right{box-shadow:-8px 0 24px rgba(15,25,40,.18)}

/* ---------- guided export mode ---------- */
/* chrome collapses: rail + left panel + inspector gone, nav shows only the
   logo and the active export step's controls */
body.export-mode .rail,
body.export-mode .left,
body.export-mode .right{display:none}
body.export-mode .canvas{left:0;right:0}
body.export-mode .top > *:not(.logo):not(.exportnav){display:none}
.exportnav{display:flex;align-items:center;gap:10px;flex:1;min-width:0;padding:0 4px}
.exportnav[hidden]{display:none}
.exd-instr{font-size:12.5px;color:var(--dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* all 3 Detail-step sliders (level of detail, marker size, stroke weight)
   share one width (Aaron's ask) rather than the detail slider standing out
   wider than the other two */
#exd-detail-slider,#exd-marker-scale,#exd-stroke-scale{width:150px}
.exd-scale-group{display:flex;align-items:center;gap:6px}
.exd-scale-group[hidden]{display:none}
.exd-scale-label{font-size:12px;color:var(--dim);white-space:nowrap}
.exd-scale-val{font-size:12px;font-weight:600;white-space:nowrap;min-width:34px}
#export-box{position:absolute;z-index:25;border:2px solid var(--accent);box-shadow:0 0 0 100000px rgba(20,30,45,.35);pointer-events:none}
/* MapLibre defaults to a grab-hand cursor at rest; features are clickable
   here, so use a pointer at rest and keep the grabbing hand only while the
   mouse is actually held down for panning. */
#map .maplibregl-canvas-container.maplibregl-interactive{cursor:pointer}
#map .maplibregl-canvas-container.maplibregl-interactive:active{cursor:grabbing}
#map{position:absolute;inset:0}
#setup-note{position:absolute;top:12px;left:50%;transform:translateX(-50%);background:#fff8e6;border:1px solid #eadfb8;color:#6e5c1e;border-radius:8px;padding:8px 14px;font-size:12px;z-index:20;max-width:80%;box-shadow:0 4px 14px rgba(28,44,60,.10)}
#setup-note code{background:rgba(0,0,0,.06);padding:1px 4px;border-radius:3px}
#measure-badge{position:absolute;left:50%;transform:translateX(-50%);bottom:18px;background:#fff;border:1px solid var(--line);border-radius:999px;padding:8px 16px;font-size:12.5px;font-weight:600;box-shadow:0 6px 20px rgba(28,44,60,.14);z-index:20}

/* ---------- bottom-right corner: privacy/terms ---------- */
.cornerbar{position:absolute;right:16px;bottom:10px;display:flex;align-items:center;gap:7px;background:rgba(255,255,255,.92);border:1px solid var(--line);border-radius:999px;padding:3px 12px;font-size:11px;color:var(--dim);z-index:20;box-shadow:0 4px 14px rgba(28,44,60,.08)}
.cornerbar a{color:var(--dim);text-decoration:none}
/* Ad box: bottom-right of the canvas, above the Privacy/Terms bar. Hidden unless eligible (free tier, Map mode; js/ads.js)
   AND an ad is in it (.has-ad). The canvas already shrinks by --rightW when the inspector docks, so it moves left on its own;
   a floating inspector (Overlay selections) leaves the canvas full width, so it is offset by hand. Desktop only: hidden
   under 1280px, where it would take too much of a docked canvas. */
.adbox{display:none;position:absolute;right:16px;bottom:44px;width:300px;max-width:300px;z-index:20;overflow:hidden;border-radius:8px;background:#fff;box-shadow:0 4px 14px rgba(28,44,60,.12)}
.adbox[data-eligible].has-ad{display:block}
body.inspector-floating .adbox{right:calc(16px + var(--rightW))}
/* Overlay tab's bottom toolbar is centered and ~700px wide: lift the box above it. */
.canvas:has(#overlaybar:not([hidden])) .adbox{bottom:90px}
/* The upgrade message lives in the box, as a strip under the ad (a sibling of #ad-box-slot, never inside the ad's own container). */
.adbox-upsell{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:8px 10px;border-top:1px solid var(--line);font-size:12px;color:var(--ink)}
.adbox-upsell .mini-btn{flex:none;white-space:nowrap}
/* While an ad shows, the box replaces the centered support banner. Only at the widths where the box is shown (below them the
   banner has to stay). */
@media (min-width:1280px){.canvas:has(#ad-box[data-eligible].has-ad) .supportbanner{display:none}}
@media (max-width:1279px){.adbox,.adbox[data-eligible].has-ad{display:none}}
body.print-mode .adbox,body.print-focus .adbox,html.mb-preview .adbox{display:none!important}

.cornerbar a:hover{color:var(--ink);text-decoration:underline}
.cornerbar .sep{color:var(--faint)}

/* ---------- support banner (free tier only, js/ads.js) ---------- */
.supportbanner{position:absolute;left:50%;bottom:18px;transform:translateX(-50%);display:flex;align-items:center;gap:16px;background:#fff;border:1px solid var(--line);border-radius:16px;padding:14px 14px 14px 22px;box-shadow:0 10px 30px rgba(28,44,60,.16);z-index:20;font-size:12.5px;max-width:min(90vw,620px)}
.supportbanner[hidden]{display:none} /* display:flex would beat the hidden attr */
.supportbanner span{color:var(--ink)}
.supportbanner .mini-btn{flex:none;white-space:nowrap}
.supportbanner-close{width:22px;height:22px;border:none;background:var(--hover);border-radius:50%;cursor:pointer;color:var(--dim);display:grid;place-items:center;flex:none}
.supportbanner-close:hover{background:var(--line-2)}
/* Same fix as .overlaybar below, same cause: on the Overlay tab the inspector
   floats over the map instead of shrinking it (.canvas goes right:0,
   body.inspector-floating), so a flat left:50% centers on the whole
   (partly-covered) canvas box and reads as shifted right of the actually-
   visible area. The banner can show on any tab (free tier, not tab-gated),
   so it hits this whenever it's up while Overlay is active. */
body.inspector-floating .supportbanner{left:calc(50% - var(--rightW) / 2)}

/* ---------- overlay draw/place toolbar (js/overlay.js, js/ui.js switchPanel;
   design/overlaybar-option-a-fullbar.html) — desktop only, docked to the
   bottom of the canvas whenever the Overlay tab is active. Section has no map
   geometry to draw/place, so it stays the lhead's own header button instead
   of living here. ---------- */
.overlaybar{position:absolute;left:50%;bottom:16px;transform:translateX(-50%);display:flex;align-items:center;gap:1px;background:#fff;border:1px solid var(--line);border-radius:12px;padding:6px;box-shadow:0 8px 24px rgba(28,44,60,.18);z-index:26}
.overlaybar[hidden]{display:none} /* display:flex above would beat the hidden attr */
/* Overlay-tab props float over the map (.canvas goes right:0, .right sits on
   top of it — see body.inspector-floating below) rather than pushing the
   canvas over, so .canvas's own box is full-viewport-width even though
   --rightW of it is visually covered by the floating panel. Centering at a
   flat 50% of that box (as if the whole thing were free map) reads as
   shifted right of the actually-visible area by half of --rightW — pull it
   back by that much whenever the panel is actually floating on top.
   body.inspector-hidden (interactive embed off + nothing selected) has no
   panel covering anything, so the plain 50% above is already correct there. */
body.inspector-floating .overlaybar{left:calc(50% - var(--rightW) / 2)}
.overlaybar button{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;width:54px;height:50px;border:none;background:none;border-radius:8px;color:var(--dim);cursor:pointer;font-size:9.5px;font-family:inherit;line-height:1}
.overlaybar button:hover{background:var(--hover);color:var(--ink)}
.overlaybar button.on{background:var(--accent-soft);color:var(--accent)}
.overlaybar .obsep{width:1px;height:30px;background:var(--line);margin:0 3px;flex:none}
/* Text/Photo overlay on Satellite (js/overlay.js SATELLITE_UNSUPPORTED,
   refreshOverlaybarState) — pointer-events:none, same convention as
   .tree.disabled/.presetlist.disabled; the title attribute (set by JS)
   carries the explanation since there's no room for a note in either bar. */
.overlaybar button.disabled,.addmenu-body button.disabled{opacity:.4;pointer-events:none}
/* the toolbar and the support banner dock to the same bottom-center spot —
   give the banner room above it rather than letting them overlap */
.canvas:has(#overlaybar:not([hidden])) .supportbanner{bottom:90px}
/* #toast (js/ui.js) is also bottom-center, and every startDraw() call shows
   one ("Click to add points…", "Drag on the map to draw a…") — at its
   default position it lands directly on top of the toolbar, at a HIGHER
   z-index (100, a #body-level sibling of .canvas, so .overlaybar's own
   z-index:26 can never outrank it from inside .canvas's stacking context),
   silently eating the very click that would start the next shape. Raise it
   clear of the bar instead. */
body:has(#overlaybar:not([hidden])) .toast{bottom:96px}

/* ---------- floating map toolbar: Search / Map style / Measure / Zoom ---------- */
/* Search is a permanent expanded bar (not a toggle) — Map style/Zoom instead
   expand THIS pill in place (icons swap out for that tool's own row) rather
   than opening a second floating box below it. Only one of .maptools-icons /
   #menu-mapstyle / #menu-detail is ever visible at a time, alongside the
   always-visible search bar, so the pill's width just follows whatever's
   currently showing. */
.maptools{position:absolute;top:12px;left:12px;display:flex;align-items:center;gap:1px;background:#fff;border:1px solid var(--line);border-radius:9px;padding:3px;box-shadow:0 8px 24px rgba(28,44,60,.16);z-index:26}
.maptools .navicon{border-radius:6px}
.maptools .divider{width:1px;height:18px;background:var(--line-2);margin:0 3px}

/* the permanently-expanded search bar — Map style/Zoom are plain dropdowns
   (.navdrop/.dropmenu below), not part of this */
.maptools-panel{display:flex;align-items:center;gap:8px;height:32px;padding:0 4px 0 8px;color:var(--dim);position:relative}
/* Map mode's bar is always expanded, so the leading icon is Print-only */
.search-ic{display:none}
.maptools-panel.locked{opacity:.45}
/* Search is never Pro-gated in Print mode (js/search.js gated() exempts it
   — moving the map to frame a poster shouldn't need a login) — the .locked
   class itself is applied from auth state alone (js/app.js), with no
   mode check, so a signed-out/free visitor's print search sat at 45%
   opacity for real, including its own results dropdown as a CHILD of this
   element (Aaron: "the suggested search dropdown is transparent" — it
   wasn't a separate bug, the whole box always rendered that dim there). */
body.print-mode .maptools-panel.locked{opacity:1}
.maptools-panel #search-input{flex:1;min-width:200px;border:none;outline:none;font:inherit;font-size:13px;color:var(--ink)}
.search-clear{border:none;background:none;color:var(--faint);cursor:pointer;display:grid;place-items:center;width:22px;height:22px;flex:none;border-radius:5px}
.search-clear:hover{background:var(--hover);color:var(--ink)}
/* display:grid above beats the browser's [hidden]{display:none}, so the clear
   × painted on an EMPTY search box — in Map mode too, since js/search.js has
   always set the attribute and trusted it. Fourth time this trap has bitten
   in this file (.railbtn, .modetoggle, .detailpop). */
.search-clear[hidden]{display:none}
/* Print's "close" button (js/search.js setClearVisibility()) — same look as
   the clear button above, shown in the opposite state (empty, not holding a
   query). display:none as the base means it never appears in Map mode,
   whose bar never collapses at all; the print-mode rules that reveal it
   live with the rest of this bar's own collapse/expand CSS. */
.search-collapse{border:none;background:none;color:var(--faint);cursor:pointer;display:none;place-items:center;width:22px;height:22px;flex:none;border-radius:5px}
.search-collapse:hover{background:var(--hover);color:var(--ink)}

/* Level-of-detail dropdown (below the zoom icon) */
.detailpop{width:200px;padding:12px;display:flex;flex-direction:column;gap:8px}
/* .detailpop's own `display:flex` otherwise ties with (and as an author rule,
   beats) the browser's default [hidden]{display:none} — needs restating here */
.detailpop[hidden]{display:none}
.detailpop input[type=range]{width:100%}
.detail-label{font-size:12px;color:var(--dim);text-align:center}

/* anchored to #searchbox itself so it stays under just the search bar's own width */
.searchresults{position:absolute;top:38px;left:0;right:0;background:#fff;border:1px solid var(--line);border-radius:8px;box-shadow:0 8px 24px rgba(28,44,60,.16);overflow:hidden}
.searchresults button{display:block;width:100%;text-align:left;border:none;background:none;padding:9px 12px;cursor:pointer;font-size:12.5px}
.searchresults button:hover{background:var(--hover)}
.searchresults .sub{display:block;font-size:11px;color:var(--faint)}
.searchresults .locsearching{padding:9px 12px;font-size:12.5px;color:var(--faint)}

/* Pro-lock popover (Search focus / Map style / Collaborate click, signed-in
   free tier) — anchored under whichever button triggered it; replaces the
   account dropdown + bottom toast that used to fire instead. */
.locktip{position:absolute;top:42px;background:#fff;border:1px solid var(--line);border-radius:8px;box-shadow:0 8px 24px rgba(28,44,60,.16);padding:12px;width:210px;z-index:27}
.locktip[hidden]{display:none}
/* #locktip specifically is viewport-fixed: MBUI.showLockTip() sets `left`/
   `top` from the anchor's own getBoundingClientRect(), not .maptools-relative
   math — Collaborate's buttons (doc-title/row menus) aren't inside .maptools
   like Search/Map style are, so that relative math put the tip in the wrong
   corner of the screen entirely (2026-08-10 bug). #pubtip keeps the plain
   .locktip absolute/top:42px behavior below — it's anchored to its own
   .btn-publish wrapper, a different mechanism (js/codeview.js).
   z-index:145 clears every panel it can be anchored against — .top (40),
   .rail/.left/.right (36/35/35), and — 2026-09-18, Aaron reported the new
   Google-style checkbox's lock-tip rendering under the Publish dialog — any
   modal .dialog-scrim (80/82) too, the first time a lock-tip's anchor ever
   lived inside a dialog rather than the main editor chrome. 145 already
   matched what the old mobile-only override below used to bump it to for
   the same "clears everything else" reason (mobile's .left bottom sheet
   climbs to 120-140) — now the one shared value, so that override is gone. */
#locktip{position:fixed;z-index:145}
.locktip p{font-size:12.5px;line-height:1.45;color:var(--ink);margin-bottom:10px}
.locktip .mini-btn{width:100%;display:flex;align-items:center;justify-content:center;text-decoration:none}

/* ---------- right inspector ---------- */
.right{position:fixed;top:var(--topH);right:0;bottom:0;width:var(--rightW);background:var(--panel);border-left:1px solid var(--line);z-index:35;display:flex;flex-direction:column;overflow-y:auto}
.right[hidden]{display:none}
.insp-head{padding:14px 16px 12px;border-bottom:1px solid var(--line)}
.insp-head .crumb{font-size:11px;color:var(--faint);margin-bottom:3px}
.insp-head .crumb:empty{display:none;margin-bottom:0}
.insp-head .title{font-size:15px;font-weight:680;letter-spacing:-.01em}
/* editable overlay-item name at the top of the right panel (overlay
   inspector only — reuses .title's font-size/weight, incl. the mobile
   override below, via the shared class) */
.insp-head .title.title-edit{display:block;width:100%;font:inherit;border:1px solid transparent;border-radius:5px;padding:2px 4px;margin:-2px -4px;background:none;color:inherit;outline:none}
.insp-head .title.title-edit:hover{border-color:var(--line-2)}
.insp-head .title.title-edit:focus{border-color:var(--accent-line);background:#fff}
.section{border-bottom:1px solid var(--line);padding:14px 16px}
.sec-top{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}
.sec-top .st{font-size:11px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:var(--dim)}
.sec-top .toggle{width:22px;height:22px;border-radius:5px;border:none;background:none;color:var(--ink);cursor:pointer;display:grid;place-items:center}
.sec-top .toggle:hover{background:var(--hover)}
.section.off{opacity:.55}
.section.off .toggle{color:var(--faint)}
.prop{display:flex;align-items:center;gap:8px;margin-bottom:9px}
.prop:last-child{margin-bottom:0}
/* CSV import's Link checkbox+picker share one .prop row (js/importcsv.js) —
   the checkbox's own .ovcheck margin is meant for a standalone stacked
   control, not a flex sibling here. .linkpick (the dropdownize()'d select's
   wrapper, hidden/shown by #csvmap-link-on) has no competing display:* rule
   of its own, so the plain [hidden] attribute needs no override here, unlike
   .prop/.mkprev/.photoprev below. */
#csvmap-link-row .ovcheck{margin-bottom:0;flex:none}
.linkpick{flex:1;min-width:0}
/* plain grouping rule between rows within a section (currently: Road's
   Geometry, above Stroke) — no label/interaction, just a visual break */
.prop-divider{height:1px;background:var(--line);margin-bottom:9px}
.prop .plabel{width:44px;font-size:12px;color:var(--dim)}
/* Fill/Stroke rows specifically: force the label to a true fixed width —
   plain `width` on a flex item is only a hint (a single unbreakable word
   like "Stroke" can still report a larger min-content size and win), which
   was throwing off the colorfield + propeye position after it, row to row */
.propcolor .plabel{flex:none;overflow:hidden;white-space:nowrap}
.colorfield{flex:1;min-width:0;display:flex;align-items:center;gap:6px;height:32px;border:1px solid var(--line-2);border-radius:6px;padding:0 6px;background:#fff;position:relative}
/* being itself a flex container, .colorfield's default min-width:auto pins
   it to its own content's min size, ignoring `flex:1` — that was pushing the
   whole row (and the propeye after it) past the section's right edge */
/* property hidden (eye toggled off) — stays in place (so the box + the
   .propeye after it never shift position) but reads as disabled */
.colorfield.off{opacity:.5;background:var(--hover)}
.colorfield.off .chip,.colorfield.off input{cursor:not-allowed}
/* same lighter --faint the disabled Width slider's "Width" title uses (see
   .prop-disabled .plabel below) — a hidden Fill/Stroke row's own title
   should read just as de-emphasized, not stay full-strength while only the
   swatch/eye next to it dim. */
.prop.off .plabel{color:var(--faint)}
/* native select arrow sits flush against the box's right edge regardless of
   padding in Chromium — appearance:none + a hand-drawn chevron is the only
   way to actually control its inset */
/* flex:1 alone doesn't let a <select> shrink past its own content's
   intrinsic width — the flex spec's automatic min-width:auto floors it at
   the widest OPTION's rendered width regardless of flex-shrink, so a
   longer-labeled dropdown (js/printdesign.js's "Photo fill": "Fill (crop
   to frame)" / "Fit (show whole photo)") pushed past the row's right edge
   while shorter ones (Horizontal/Vertical etc.) happened to fit and never
   exposed it (Aaron, 2026-09-19: "the fill dropdown inputbox is too long
   ... align the right side of other elements"). min-width:0 releases that
   floor; the ellipsis trio keeps an overflowing VALUE readable instead of
   just clipped when the shrunk width still can't show it in full. */
.prop select.dlg-input{flex:1;width:auto;min-width:0;height:32px;appearance:none;-webkit-appearance:none;padding-right:30px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236b7480' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 8px center;background-size:14px}
.colorfield .chip{width:16px;height:16px;border-radius:4px;box-shadow:inset 0 0 0 1px rgba(20,30,45,.16);flex:none;cursor:pointer;border:none;padding:0}
.colorfield .chip:disabled{cursor:not-allowed}
.colorfield .hex{font-size:11.5px;font-variant-numeric:tabular-nums;flex:1;border:none;outline:none;background:none;font-family:inherit;min-width:0;text-transform:uppercase}
/* propcolor.mixed: a parent row (e.g. Landscape) whose children currently
   disagree — the swatch still paints a plain neutral fallback (inline style
   from MBColorPicker.paintChip, so a CSS background here would just lose to
   it), so 'Mixed' in dimmed italic text is the only signal, same spot a real
   hex value would sit. */
.propcolor.mixed .hex{color:var(--faint);font-style:italic;text-transform:none}
.propeye{width:22px;height:22px;flex:none;border-radius:5px;border:none;background:none;color:var(--ink);cursor:pointer;display:grid;place-items:center}
.propeye:hover{background:var(--hover)}
.propeye.off{color:var(--faint)}
/* fixed-width trailing column, shared with .rv (the Opacity/Width readout)
   below — this is what actually makes .colorfield and the slider render
   the same width: both rows reserve the identical trailing-column width,
   so their flex:1 control gets the identical remaining space. The icon
   itself stays a normal 22px square button, right-aligned within the
   wider column rather than stretched to fill it. */
.propaccessory{width:36px;flex:none;display:flex;justify-content:flex-end;align-items:center}
.insp-empty{padding:20px 16px;color:var(--faint);font-size:12.5px;line-height:1.5}

/* ---------- shared color picker popover (js/colorpicker.js) ---------- */
.cpicker{position:fixed;z-index:200;width:232px;background:#fff;border:1px solid var(--line);border-radius:10px;box-shadow:0 12px 40px rgba(15,25,40,.25);padding:12px}
/* Solid/Gradient switch and the gradient bar (js/colorpicker.js). Both are
   hidden unless the caller passed allowGradient, so every existing picker
   looks exactly as it did. */
.cp-modes{display:flex;gap:2px;background:var(--hover);border-radius:7px;padding:2px;margin-bottom:10px}
.cp-modes[hidden]{display:none}
.cp-modes button{flex:1;border:none;background:none;border-radius:5px;padding:5px 0;font:650 11.5px/1 inherit;color:var(--dim);cursor:pointer}
.cp-modes button.on{background:#fff;color:var(--ink);box-shadow:0 1px 3px rgba(20,30,45,.16)}
.cp-gradbar{margin-bottom:10px}
.cp-gradbar[hidden]{display:none}
.cp-gradprev{height:22px;border-radius:6px;border:1px solid var(--line-2)}
.cp-gradstops{display:flex;align-items:center;gap:6px;margin-top:7px}
/* the two stop swatches double as "which end am I editing" radio buttons */
.cp-stop{width:22px;height:22px;flex:none;border-radius:5px;border:1.5px solid var(--line-2);cursor:pointer;padding:0}
.cp-stop.on{border-color:var(--accent);box-shadow:0 0 0 1px var(--accent)}
.cp-angle{flex:1;min-width:0}
.cp-angleval{width:32px;flex:none;text-align:right;font-size:11px;color:var(--dim)}
/* a gradient can't be typed, so the field shows "Gradient" and goes read-only */
.colorfield .hex[readonly]{color:var(--dim);cursor:default}
.cpicker[hidden]{display:none}
.cp-sl{position:relative;width:100%;height:140px;border-radius:6px;cursor:crosshair;margin-bottom:10px;user-select:none;touch-action:none}
.cp-sl-dot{position:absolute;width:14px;height:14px;border-radius:50%;border:2px solid #fff;box-shadow:0 0 0 1px rgba(0,0,0,.4),0 1px 4px rgba(0,0,0,.3);transform:translate(-50%,-50%);pointer-events:none}
.cp-controls{display:flex;align-items:center;gap:8px;margin-bottom:10px}
.cp-eyedrop{width:28px;height:28px;flex:none;border:none;background:none;border-radius:6px;color:var(--dim);cursor:pointer;display:grid;place-items:center}
.cp-eyedrop:hover{background:var(--hover)}
.cp-eyedrop[hidden]{display:none}
.cp-sliders{flex:1;display:flex;flex-direction:column;gap:10px;min-width:0}
.cp-hue{position:relative;height:12px;border-radius:999px;cursor:pointer;user-select:none;touch-action:none}
.cp-hue{background:linear-gradient(to right,#f00,#ff0,#0f0,#0ff,#00f,#f0f,#f00)}
.cp-slider-dot{position:absolute;top:50%;width:14px;height:14px;border-radius:50%;background:#fff;border:2px solid #fff;box-shadow:0 0 0 1px rgba(0,0,0,.4),0 1px 4px rgba(0,0,0,.3);transform:translate(-50%,-50%);pointer-events:none}
.cp-fields{display:flex;gap:8px}
/* format picker (Hex/RGB/HSL) — same appearance:none + hand-drawn-chevron
   treatment as .prop select.dlg-input, just sized for the 232px popover */
.cp-format{flex:none;width:62px;height:30px;border:1px solid var(--line-2);border-radius:6px;padding:0 18px 0 8px;font:inherit;font-size:12px;color:var(--ink);background-color:#fff;appearance:none;-webkit-appearance:none;cursor:pointer;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236b7480' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 5px center;background-size:12px}
/* Hex is a single free-text field (unchanged from before); RGB/HSL instead
   render 3 short per-channel fields side by side, divided by hairlines, so
   switching format swaps what's inside the same box rather than changing
   its shape. */
.cp-valuefield{flex:1;display:flex;align-items:center;height:30px;border:1px solid var(--line-2);border-radius:6px;min-width:0;overflow:hidden}
.cp-valuefield input{flex:1;width:0;min-width:0;border:none;outline:none;font-size:12.5px;font-family:inherit;height:100%;background:none;text-align:center;padding:0 2px}
.cp-valuefield.cp-hexmode input{text-align:left;padding:0 8px;text-transform:uppercase}
.cp-valuefield:not(.cp-hexmode) input:not(:first-child){border-left:1px solid var(--line-2)}

/* ---------- photo crop dialog (js/photocrop.js) ---------- */
.pcrop-scrim{z-index:210} /* above the panels, the print inspector and the other dialogs (mobile lifts those to 200) */
.pcrop{background:var(--panel,#fff);border-radius:12px;box-shadow:0 12px 32px rgba(0,0,0,.28);padding:14px;width:min(560px,94vw);max-height:94vh;display:flex;flex-direction:column;gap:10px}
.pcrop-head{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap}
.pcrop-title{font-weight:700;font-size:14px;color:var(--ink)}
.pcrop-ratios{display:flex;gap:4px;flex-wrap:wrap}
.pcrop-ratio{border:1px solid var(--line-2);background:#fff;border-radius:6px;padding:4px 9px;font-size:12px;cursor:pointer;color:var(--ink)}
.pcrop-ratio.on{background:var(--accent);border-color:var(--accent);color:#fff}
.pcrop-stagewrap{display:grid;place-items:center;background:#11161c;border-radius:8px;min-height:160px;padding:10px;overflow:hidden}
.pcrop-stage{position:relative;display:inline-block;line-height:0;touch-action:none;user-select:none}
.pcrop-stage[hidden]{display:none}
.pcrop-img{display:block;max-width:100%;max-height:min(58vh,520px);width:auto;height:auto;user-select:none;-webkit-user-drag:none}
.pcrop-msg{color:#cfd6dd;font-size:13px;line-height:1.4;padding:20px;text-align:center}
.pcrop-msg[hidden]{display:none}
/* the huge box-shadow dims everything outside the crop; the stage wrapper clips it */
.pcrop-box{position:absolute;box-shadow:0 0 0 9999px rgba(10,14,20,.58);outline:2px solid #fff;cursor:move;touch-action:none}
.pcrop-h{position:absolute;width:14px;height:14px;margin:-7px;background:#fff;border:2px solid var(--accent);border-radius:3px;touch-action:none;box-sizing:border-box}
.pcrop-h-nw{left:0;top:0;cursor:nwse-resize}.pcrop-h-n{left:50%;top:0;cursor:ns-resize}.pcrop-h-ne{left:100%;top:0;cursor:nesw-resize}
.pcrop-h-e{left:100%;top:50%;cursor:ew-resize}.pcrop-h-se{left:100%;top:100%;cursor:nwse-resize}.pcrop-h-s{left:50%;top:100%;cursor:ns-resize}
.pcrop-h-sw{left:0;top:100%;cursor:nesw-resize}.pcrop-h-w{left:0;top:50%;cursor:ew-resize}
.pcrop-box.locked .pcrop-h-n,.pcrop-box.locked .pcrop-h-e,.pcrop-box.locked .pcrop-h-s,.pcrop-box.locked .pcrop-h-w{display:none} /* a fixed ratio is resized from its corners */
@media (pointer:coarse){.pcrop-h{width:24px;height:24px;margin:-12px}}
.pcrop-foot{display:flex;align-items:center;gap:8px}
.pcrop-size{margin-right:auto;font-size:12px;color:var(--dim);font-variant-numeric:tabular-nums}
.pcrop-foot .mini-btn{height:30px;padding:0 16px;font-size:13px}
.pcrop-foot .pcrop-apply{background:var(--accent);border-color:var(--accent);color:#fff}
.pcrop-foot .pcrop-apply:disabled{opacity:.5;cursor:default}

/* ---------- export dialog ---------- */
.dialog-scrim{position:fixed;inset:0;background:rgba(20,28,38,.35);z-index:80;display:grid;place-items:center}
.dialog-scrim[hidden]{display:none}

/* First-time walkthrough (js/nux.js) — a spotlight cutout, not a screenshot
   slideshow. .nux-hole is a transparent rect whose box-shadow's huge spread
   dims the ENTIRE rest of the screen in one paint (no separate full-page
   dimmer element needed); its position/size are set inline per step. .nux
   itself is the click-catcher, above every panel/rail/dialog this app has. */
.nux{position:fixed;inset:0;z-index:200}
.nux[hidden]{display:none}
.nux-hole{position:fixed;border-radius:10px;box-shadow:0 0 0 9999px rgba(15,23,32,.6);transition:left .25s ease,top .25s ease,width .25s ease,height .25s ease,opacity .25s ease;pointer-events:none}
.nux-card{position:fixed;width:280px;background:var(--panel,#fff);border-radius:12px;box-shadow:0 12px 32px rgba(0,0,0,.28);padding:14px 16px;transition:left .25s ease,top .25s ease}
.nux-title{font-weight:700;font-size:14px;margin-bottom:6px;color:var(--ink)}
.nux-body{font-size:13px;color:var(--dim);line-height:1.45;margin-bottom:14px}
.nux-foot{display:flex;align-items:center;justify-content:space-between;gap:10px}
.nux-skip{background:none;border:none;color:var(--dim);cursor:pointer;font-size:13px;padding:6px 2px}
.nux-skip:hover{color:var(--ink)}
.nux-next{background:var(--accent);color:#fff;border:none;border-radius:8px;padding:7px 16px;cursor:pointer;font-size:13px;font-weight:600}
.nux-dots{display:flex;gap:5px}
.nux-dot{width:6px;height:6px;border-radius:50%;background:var(--line-2)}
.nux-dot.on{background:var(--accent)}
/* The save dialog can be triggered from inside another open dialog (Publish
   tapped with no saved map, or with unsaved edits — js/mymaps.js's
   saveThenRun) — same z-index as every other .dialog-scrim would leave DOM
   order deciding the stack, and #savemap-scrim comes BEFORE #code-scrim in
   the markup, so it would render underneath instead of on top of it. */
#savemap-scrim{z-index:82}
/* Same bug, same fix, one more caller: the concurrent-save conflict dialog
   (js/mymaps.js askConflict()) fires from inside that same saveThenRun()
   path — Publish tapped, saveThenRun's save hits a real conflict — and
   #conflict-scrim also sits before #code-scrim in the markup. Without its
   own bump it rendered behind the still-open Publish dialog (Aaron's
   report, 2026-08-06): "This map changed" opening underneath, invisible
   until Publish was dismissed first. */
#conflict-scrim{z-index:82}
.dialog{width:430px;max-width:94vw;background:#fff;border-radius:12px;box-shadow:0 20px 60px rgba(15,25,40,.3);overflow:hidden}
.dlg-head{display:flex;align-items:center;justify-content:space-between;padding:14px 16px;border-bottom:1px solid var(--line);font-weight:680;font-size:14px}
/* .headbtn is only ever styled under .lhead (the left-panel headers), so a
   .headbtn sitting in a DIALOG head had no styling at all and rendered as the
   browser's default button — a grey bordered box next to a borderless close
   (Aaron's report on the Orders dialog's Refresh). Header actions share the
   close button's rule outright rather than copying it, so the two can't drift. */
.dlg-close,.dlg-head-actions .headbtn{width:28px;height:28px;border:none;background:none;display:grid;place-items:center;cursor:pointer;color:var(--dim);border-radius:6px;padding:0}
.dlg-close:hover,.dlg-head-actions .headbtn:hover{background:var(--hover)}
.dlg-body{padding:16px}
.dlg-foot{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:12px 16px;border-top:1px solid var(--line)}
.dlg-foot-actions{display:flex;gap:8px}
.dialog.wide{width:640px}
/* Flex column + viewport cap so a short browser window can't push the
   Preview/Publish row (position:sticky, .pub-publishrow below) off the
   bottom — the body scrolls instead. On a tall window nothing changes:
   .code-hosted keeps its 460px preferred height. */
.dialog.pubdialog{width:480px;display:flex;flex-direction:column;max-height:calc(100vh - 24px)}
.pubdialog .dlg-head,.pubdialog .codetabs{flex:none}
.pubdialog .code-body{display:flex;flex-direction:column;flex:1;min-height:0;max-height:none}

/* ---- CSV import dialog: three columns (js/importcsv.js, index.html) ----
   design/csvimport3col-option-b-stickypreview.html, Aaron's pick 2026-08-07
   ("reuse the existing UI as much as possible"). Set up (Type + Visual —
   the purely stylistic, whole-file choices) / Data (Location + On the map
   + In the popup — every "which column holds what" question, Location
   included, moved here the same day) / Preview (always two boxes, stacked:
   Marker-or-Photo above Popup). Type is the exact .ctrl-cards picker
   js/overlay.js's own merged Marker/Photo Appearance tab uses
   (typeCardsSection) — same component, not a lookalike. */
.dialog.csvdlg{width:980px}
/* Both photo-import dialogs get a flat 634px height (Aaron's ask) instead of
   flexing to their own content, so the two steps feel like one consistent
   modal rather than resizing between them. #pimport-preview-dialog already
   had .csvprevdlg's flex-column + .csvpreview{flex:1} shape (needed so its
   own footer pins to the bottom); #pimport-dialog didn't, since #csvmap-
   dialog (the shell it's based on) has never needed a fixed height — given
   one here, so its .csv3col fills the remaining space instead of leaving
   the footer floating mid-dialog with blank space below it. */
#pimport-dialog,#pimport-preview-dialog{height:634px}
#pimport-dialog{display:flex;flex-direction:column}
#pimport-dialog .csv3col{flex:1;min-height:0;overflow-y:auto}
/* align-items:stretch (the default) so col-setup and col-data always match
   the grid row's height, same reasoning .csvsplit's stretch used to give
   .csvdata/.csvlook — Preview opts back OUT of that via its own
   align-self below, since unlike those two it should size to its own
   short content, not be stretched to match them. Equal 1fr/1fr (Aaron's
   ask, 2026-08-13) — the two columns no longer split "style vs data"
   (col-setup was a narrow 220px, col-data a wider 1fr, back when col-setup
   held far less); they now each mirror one whole panel tab (Appearance,
   Popup) and neither reliably holds less content than the other. Preview
   stays a fixed 240px — its own two stacked boxes size to themselves, not
   to whatever's left over. */
.csv3col{display:grid;grid-template-columns:1fr 1fr 240px}
.colhead{padding:14px 16px 10px;font-size:11px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:var(--dim)}
/* The row's Name, styled like the REAL floating caption it becomes on the
   map (it.style.label -> mtitle, js/overlay.js LYR_MARKER: dark text on a
   white halo, no background box) rather than any UI chrome — text-stroke
   is this preview's stand-in for MapLibre's text-halo-width. Positioned by
   renderPreview() (js/importcsv.js) on either side of the icon/photo, same
   above-when-bare / below-when-labeled-or-photo flip LYR_MARKER's own
   text-anchor does. */
.prevname{font-size:13px;font-weight:600;color:#1e2328;text-align:center;max-width:200px;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  -webkit-text-stroke:3px #fff;paint-order:stroke fill}
.prevname:empty{display:none}
.col-setup{min-width:0;border-right:1px solid var(--line);max-height:min(58vh,520px);overflow-y:auto}
/* Same cap, same scroll, as .col-setup — without it, a short viewport
   shrinks whichever column has somewhere to put the overflow while the
   other keeps its own natural minimum height, so the taller one drives the
   grid row height and the shorter one's own lower rows scroll out of view
   even though the dialog is clearly tall enough. Matching caps mean
   neither side can out-grow the other. */
.col-data{min-width:0;border-right:1px solid var(--line);
  max-height:min(58vh,520px);overflow-y:auto}
/* Preview is a plain sibling column, not nested inside either scroller
   above — so it's already always in view regardless of how far Set up or
   Data have scrolled internally, with no position:sticky needed to
   achieve that. align-self:start keeps it sized to its own short content
   instead of being stretched (the row default) to match the taller,
   capped columns beside it. */
.col-preview{min-width:0;align-self:start}
/* #csvmap-note sits here, below .csv3col, not inside either scrolling
   column — dialog-level ("N rows skipped"), so it stays fully visible
   regardless of any column's own scroll position, instead of getting
   scrolled down to a sliver of its own top border peeking above the
   footer. */
.csvdlg > .dlg-note{margin:14px 16px 0}
.csvfile{color:var(--dim);font-weight:400}
.csvprev-btn{margin-left:10px;height:22px;padding:0 9px;border-radius:999px;border:1px solid var(--line-2);
  background:var(--panel);color:var(--dim);font:inherit;font-size:11.5px;font-weight:600;cursor:pointer;vertical-align:middle}
.csvprev-btn:hover{color:var(--ink);border-color:var(--dim)}

/* ---- CSV file-preview modal (js/importcsv.js #csvpreview-dialog) ----
   A separate modal the same width as .csvdlg, with its height set inline by
   js/importcsv.js to match #csvmap-dialog exactly — so it fills flex:1 rather
   than assuming a fixed height the way a 210px inline insert could. No
   dlg-foot — the header close button is the only way out (Aaron's call). */
.dialog.csvprevdlg{display:flex;flex-direction:column}
.csvpreview{flex:1;min-height:0;display:flex;flex-direction:column;background:var(--canvas)}
.csvpreview-scroll{flex:1;min-height:0;overflow:auto}
#csvmap-preview-table{border-collapse:collapse;font-size:11.5px;white-space:nowrap}
#csvmap-preview-table th,#csvmap-preview-table td{border-right:1px solid var(--line);border-bottom:1px solid var(--line);
  padding:5px 10px;text-align:left;max-width:220px;overflow:hidden;text-overflow:ellipsis}
/* the header row stays put while the body scrolls under it */
#csvmap-preview-table th{position:sticky;top:0;background:var(--panel);color:var(--dim);font-weight:600;z-index:1}
#csvmap-preview-table td{color:var(--ink);background:var(--panel)}
.csvpreview-foot{padding:6px 10px;font-size:11px;color:var(--faint);border-top:1px solid var(--line)}
.col-setup .section:last-of-type,.col-data .section:last-of-type{border-bottom:none}
/* One shared width (2026-08-13, was 70px/80px split back when col-setup was
   narrower than col-data) — both columns are the same width now, and share
   longer labels alike ("Aspect ratio", "Latitude"/"Longitude"). */
.col-setup .prop .plabel,.col-data .prop .plabel{width:80px;flex:none}
.col-setup .prop select.dlg-input,.col-data .prop select.dlg-input{height:32px}
/* Type's .ctrl-cards (js/overlay.js's own typeCardsSection reuses this exact
   markup) defaults to a 2-up grid sized for the export dialog's much wider
   .split-controls — forced to one column here, same override #insp-body's
   own Type picker already needs at a similarly narrow width. */
.col-setup .ctrl-cards{grid-template-columns:1fr}
/* .prop select is sized for single-line dropdowns — a multi-select needs its
   natural height back or its options overlap inside 32px (was a real bug in
   the first version of this dialog, caught by the design spike) */
.csv3col .prop select[multiple].dlg-input{height:auto;min-height:76px;padding:6px 8px;background-image:none}
/* Multi-column mappings (Address, Details). A native <select multiple> needs
   ctrl/cmd-click to pick more than one — undiscoverable, and impossible on
   touch — and it stands as tall as the column count, so those two rows dwarfed
   every single-select row beside them. This is a normal-looking one-line field
   that opens a checklist instead; the row above it stays a plain .prop. */
.multipick{flex:1;position:relative;min-width:0}
/* The native <select> stays in the DOM as the source of truth (currentMapping
   reads it, dev/smoke.js drives it) — it's just never shown. Not display:none:
   a select that isn't rendered can't be focused or driven the same way. */
.skinned-select{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none;left:-9999px}
/* single-choice flavour: one option wins, so the menu has no Done — picking
   closes it, same as a native select */
.onepick .mp-opt .mp-box{border-radius:50%}
.mp-field{width:100%;height:32px;border:1px solid var(--line-2);border-radius:6px;padding:0 30px 0 10px;
  font:inherit;font-size:13px;text-align:left;background:var(--panel);color:var(--ink);cursor:pointer;outline:none;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236b7480' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat:no-repeat;background-position:right 8px center;background-size:14px}
.mp-field:focus-visible,.multipick.open .mp-field{border-color:var(--accent-line)}
/* The selection can be several column names long — clip it rather than let the
   field grow and shove the layout around. */
.mp-text{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.mp-text.empty{color:var(--faint)}
/* Fixed, not absolute: .col-setup/.col-data are overflow-y:auto scrollers, so
   an absolutely positioned menu gets clipped at the dialog edge — and
   Details is the second-to-last row, i.e. exactly where that bites.
   Coordinates come from the field's rect at open time (see place() in
   js/importcsv.js), which also lets it flip
   above when there's no room below. z-index clears .dialog-scrim's 80. */
.mp-menu{position:fixed;z-index:90;background:var(--panel);
  border:1px solid var(--line-2);border-radius:8px;box-shadow:0 8px 24px rgba(20,30,45,.14);overflow:hidden}
.mp-menu[hidden]{display:none}
.mp-list{max-height:190px;overflow-y:auto;padding:4px}
.mp-opt{display:flex;align-items:center;gap:8px;width:100%;padding:6px 8px;border:none;background:none;
  border-radius:5px;font:inherit;font-size:12.5px;color:var(--ink);text-align:left;cursor:pointer}
.mp-opt:hover{background:var(--hover)}
.mp-box{width:15px;height:15px;flex:none;border:1px solid var(--line-2);border-radius:4px;background:var(--panel);
  display:grid;place-items:center}
.mp-box svg{width:11px;height:11px;display:none}
.mp-opt.on .mp-box{background:var(--accent);border-color:var(--accent)}
.mp-opt.on .mp-box svg{display:block;stroke:#fff}
.mp-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.mp-foot{display:flex;justify-content:flex-end;padding:6px;border-top:1px solid var(--line)}
.mp-foot .mini-btn{height:26px;padding:0 14px;font-size:12px}
.reqdot{display:inline-block;width:5px;height:5px;border-radius:50%;background:var(--accent);vertical-align:middle;margin-left:5px}
.secnote{font-size:11.5px;color:var(--faint);line-height:1.45;margin:-4px 0 11px}
/* #csvmap-geonote is nested inside the Location .section, which already
   supplies 16px of horizontal padding, so a margin here needs zero of its
   own to match the Address row's width, and margin-top:0 would collapse
   away that row's own spacing — 9px top matches the standard gap between
   stacked .prop rows instead. */
#csvmap-geonote{margin:9px 0 0}

/* No flex:1/stretch mechanism here any more (2026-08-07) — .col-preview is a
   plain sibling column (align-items:start on .csv3col), not force-matched
   to .col-setup/.col-data's height the way .csvlook used to be, so there's
   no leftover space for it to absorb between Marker/Photo mode. min-height
   just keeps a consistent visual footprint either way. */
.previewpad{margin:0 16px 16px;min-height:168px;border-radius:8px;border:1px solid var(--line-2);position:relative;
  background:#eef2ee;
  background-image:linear-gradient(rgba(255,255,255,.6) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.6) 1px,transparent 1px),linear-gradient(120deg,#e7eee6 0 40%,#eaf0f4 40% 100%);
  background-size:22px 22px,22px 22px,100% 100%;display:grid;place-items:center;overflow:hidden;padding:16px 0}
/* Stacked (2026-08-07, Aaron's ask — was side by side), Marker-or-Photo
   above Popup, so each box gets the column's own full width instead of
   splitting it. */
.prevstage{display:flex;flex-direction:column;align-items:center;gap:16px;max-width:100%;padding:0 10px}
/* Marker/Photo and Popup each sit in their own column so the caption
   underneath (.prevlabel) lands under its own box rather than beside both. */
.prevcol{display:flex;flex-direction:column;align-items:center;gap:6px}
.prevlabel{font:700 10px/1.3 inherit;letter-spacing:.05em;text-transform:uppercase;color:var(--faint)}
/* the click-popup, at the same scale the marker previews at */
.popprev{width:132px;background:#fff;border-radius:8px;box-shadow:0 3px 14px rgba(15,25,40,.22);
  overflow:hidden;text-align:left;flex:none}
.popprev[hidden]{display:none}
.popprev img{display:block;width:100%;height:52px;object-fit:cover;background:var(--hover)}
.popprev .pp-body{padding:7px 9px}
.popprev .pp-title{font-size:11.5px;font-weight:700;color:var(--ink);line-height:1.25;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.popprev .pp-desc{font-size:10.5px;color:var(--dim);line-height:1.45;margin-top:3px;white-space:pre-line;
  display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
/* Real buttons (Aaron's ask), not a plain link line — same shape as the
   actual published popup (pub.html / js/overlay.js buildPreviewPopupCard's
   .mb-popup-actions/.mb-popup-btn), just scaled down for this 132px mockup
   instead of reusing those classes at their real (220-380px) size. */
.popprev .pp-actions{display:flex;flex-direction:column;gap:4px;margin-top:6px}
.popprev .pp-btn{box-sizing:border-box;display:flex;align-items:center;justify-content:center;gap:4px;
  width:100%;padding:5px 7px;border-radius:5px;font:600 10px/1.2 inherit}
.popprev .pp-btn svg{width:11px;height:11px;flex:none}
.popprev .pp-btn-nav{background:#2d7ff9;color:#fff}
.popprev .pp-btn-link{background:#eef1f4;color:#1e2328}
/* the panel's 5-column icon grid assumes a 240px panel; pack by size instead
   so the buttons stay square regardless of .col-setup's own width */
.col-setup .iconpick{grid-template-columns:repeat(auto-fill,minmax(34px,1fr))}
#csvmap-style[hidden]{display:none}
.photonote{margin:12px 0 0}

/* live marker preview — mirrors buildLabelIcon(): icon and text in ONE box
   with a downward tail. No label text means a plain round icon badge. */
.mkprev{position:relative;display:inline-flex;align-items:center;gap:6px;background:var(--accent);color:#fff;
  border-radius:999px;padding:6px 12px 6px 9px;box-shadow:0 0 0 2px #fff,0 2px 8px rgba(15,25,40,.25);
  font-size:12.5px;font-weight:600;line-height:1;max-width:232px}
.mkprev svg{width:15px;height:15px;fill:currentColor;flex:none}
.mkprev .mktext{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.mkprev::after{content:"";position:absolute;left:50%;top:100%;transform:translateX(-50%);
  border:5px solid transparent;border-top-color:#fff;border-bottom:none}
/* No label: the real marker is the glyph alone (buildIcon) — no pill, no
   circular container, no callout tail. Sized up so it reads at preview scale,
   and outlined the way buildIcon outlines it. */
.mkprev.bare{background:none!important;box-shadow:none;padding:0;border-radius:0}
.mkprev.bare .mktext{display:none}
.mkprev.bare::after{display:none}
.mkprev.bare svg{width:44px;height:44px;fill:currentColor;stroke:#fff;stroke-width:1.4;
  paint-order:stroke fill;filter:drop-shadow(0 1px 2px rgba(15,25,40,.3))}
.mkprev.bare.sm svg{width:34px;height:34px}
.mkprev.bare.lg svg{width:56px;height:56px}
.mkprev.sm{font-size:11px;padding:4px 9px 4px 7px}
.mkprev.sm svg{width:12px;height:12px}
.mkprev.lg{font-size:14.5px;padding:9px 16px 9px 12px}
.mkprev.lg svg{width:20px;height:20px}
.photoprev{width:96px;height:72px;border-radius:6px;background:#fff;border:1px solid var(--line-2);
  display:grid;place-items:center;color:var(--faint);box-shadow:0 2px 8px rgba(15,25,40,.18);overflow:hidden}
.photoprev img{width:100%;height:100%;object-fit:cover;display:block}
/* import dialogs' Shape (circle: no tail, round) and Photo fill (fit: whole photo) picks */
.photoprev.circle{width:72px;border-radius:50%}
.photoprev.fit img{object-fit:contain}
/* explicit [hidden] overrides — a display value in the rule above otherwise
   wins over the attribute and the element stays visible. Fourth time this
   exact trap has bitten in this codebase (.ovpz-urlrow, .overlaybar,
   .supportbanner); any new display:* rule here needs its own pair. */
.mkprev[hidden],.photoprev[hidden]{display:none}

.csvcount{font-size:12px;color:var(--dim)}
.csvcount b{color:var(--ink);font-variant-numeric:tabular-nums}
.csvprogress{flex:1;display:flex;align-items:center;gap:10px;min-width:0}
.csvprogress[hidden]{display:none}

/* Photo import — step 1, arrange photos (js/importphotos.js). One card per
   location cluster, holding a real drag-to-reorder grid — the ONLY place
   order is ever set now (2026-08-19, reversing the previous round's step-2
   pickers): reuses .ovgallery/.ovgthumb (the shipped popup gallery's own
   grid, design/photogallery-option-a-coverbadge.html — no chevrons, no
   cover badge, order IS the meaning) sized down to compact thumbnails
   instead of that widget's usual dialog-width tiles, since several of these
   grids stack in one scrolling list here. */
/* The instruction line reads as part of the header (Aaron's ask), not the
   grey scrollable body below — .dlg-head's own border-bottom moves down
   onto this element instead of sitting between the two, and an explicit
   white background guards against ever reading as floating over the
   .csvpreview grey zone that follows. */
#pimport-preview-dialog .dlg-head{border-bottom:none}
.pimg-instr{background:#fff;margin:0;padding:0 16px 14px;border-bottom:1px solid var(--line)}
.pimg-groups{display:flex;flex-direction:column;gap:12px;padding:14px 16px}
.pimg-group{border:1px solid var(--line);border-radius:8px;padding:12px 14px}
.pimg-group-head{display:flex;align-items:baseline;justify-content:space-between;gap:10px;margin-bottom:10px}
.pimg-group-name{font-size:13px;font-weight:700;color:var(--ink)}
.pimg-group-count{font-size:11.5px;color:var(--faint);flex:none}
.pimg-group .ovgallery{display:flex;flex-wrap:wrap;grid-template-columns:none;position:relative}
.pimg-group .ovgthumb{width:56px;height:56px;flex:none;cursor:grab}
/* Drop-position indicator (2026-08-19, Aaron's ask): sits IN THE GAP
   between two tiles, not painted on one edge of a single photo — a
   same-edge divider read as highlighting that photo rather than marking
   an insertion point. One shared element per grid, positioned in JS
   (js/importphotos.js's dropIndicator/wireReorder) rather than a
   pseudo-element on the target tile, because .ovgthumb clips its own
   overflow for the rounded corners and the gap always sits outside that
   box. pointer-events:none so it never itself becomes a drop target. */
.pimg-drop-indicator{position:absolute;width:3px;border-radius:2px;background:var(--accent);
  transform:translateX(-50%);pointer-events:none;z-index:2}
.pimg-drop-indicator[hidden]{display:none}

.csvmap-progress-track{flex:1;height:6px;border-radius:3px;background:var(--line);overflow:hidden;min-width:60px}
.csvmap-progress-bar{height:100%;width:0;background:var(--accent);transition:width .2s}
#csvmap-progress-text{flex:none}
/* A later, unscoped `.dlg-foot{justify-content:flex-end}` rule (near
   .sharefoot, ~line 1746) shadows the base space-between rule for every
   dialog footer in the app — real, pre-existing, and out of scope to fix
   broadly here (several other dialogs may have been visually tuned around
   it). Scoped back to space-between just for these two, ID-specificity so
   source order can't flip it again — #pimport-back needs to sit at the
   true left edge, opposite Cancel/Import, not bunched against them. */
#pimport-dialog .dlg-foot,#pimport-preview-dialog .dlg-foot{justify-content:space-between}

/* export dialog: order-summary split (2026-07-13) — fixed height so
   switching Format/Output/size never resizes the box; shorter panels
   (vector) just leave blank space below, taller ones scroll within
   .split-controls instead of growing. #export-go is unique to this
   dialog (no other dialog has one), so it's safe to fully own its sizing
   here rather than sharing a generic footer-button rule. */
.dialog.split{width:720px;max-width:96vw;height:min(700px,88vh);display:flex;flex-direction:column}
.split-body{display:flex;flex:1;min-height:0}
.split-controls{flex:1;padding:16px;overflow-y:auto;min-width:0}
.split-summary{width:250px;flex:none;border-left:1px solid var(--line);background:var(--hover);display:flex;flex-direction:column;padding:16px;overflow-y:auto}
#export-go{width:100%;height:38px;font-size:13px;white-space:nowrap;margin-top:auto}

/* gradient + crosshair lines are a loading placeholder — replaced by a real
   thumbnail of the drawn box once renderPreviewThumb() finishes (a tiny
   client-side MapLibre render, same technique as the free 2K export, so it
   costs nothing); .has-image drops the placeholder look once it lands */
.preview-swatch{border:1px solid var(--line-2);border-radius:8px;background:linear-gradient(135deg,#dfe7ee,#c9d6e2) center/cover;aspect-ratio:3/2;margin-bottom:14px;position:relative;overflow:hidden;cursor:crosshair}
.preview-swatch::after{content:"";position:absolute;inset:0;background:
  linear-gradient(90deg,transparent 31%,rgba(255,255,255,.5) 32%,transparent 33%),
  linear-gradient(0deg,transparent 61%,rgba(255,255,255,.5) 62%,transparent 63%)}
.preview-swatch.has-image::after{display:none}

/* draggable dot marking which spot the close-up preview below is centered
   on — pointer-events:none because the drag listeners live on
   .preview-swatch itself (dragging anywhere moves the dot, not just a grab
   handle), see MBExport's pointer handlers */
.preview-marker{position:absolute;left:50%;top:50%;width:14px;height:14px;margin:-8px 0 0 -8px;border-radius:50%;background:var(--accent);border:2px solid #fff;box-shadow:0 0 0 1px rgba(20,30,45,.3),0 1px 3px rgba(20,30,45,.35);pointer-events:none;z-index:1}
.preview-marker[hidden]{display:none}

/* speech-bubble hint pointing at the dot — replaces a standalone "Check a
   close-up spot" button (Aaron, 2026-07-24): the dot itself is already the
   whole interaction (drag to move, release to open), so the affordance
   belongs on the dot, not a separate control below it. Shares the dot's
   left/top (see positionCropMarker in export.js) and offsets upward. */
.marker-tip{position:absolute;left:50%;top:50%;transform:translate(-50%,calc(-100% - 12px));background:#1e2328;color:#fff;font-size:10.5px;font-weight:600;padding:4px 9px;border-radius:6px;white-space:nowrap;pointer-events:none;z-index:2}
.marker-tip::after{content:"";position:absolute;left:50%;top:100%;transform:translateX(-50%);border:5px solid transparent;border-top-color:#1e2328}
.marker-tip[hidden]{display:none}

/* collapsed by default (Aaron's call, 2026-07-24 — the earlier
   always-stacked version was too crowded): the panel is positioned OVER
   the recap/price/note/button below rather than pushing them down —
   .closeup-anchor has no in-flow content of its own (.crop-panel is
   position:absolute), so it contributes zero height to the rail, and
   .crop-panel's top:100% anchors right where the swatch's own margin
   leaves off regardless of the swatch's aspect-ratio-driven height */
.closeup-anchor{position:relative}

/* the floating panel itself: opaque card + shadow so it reads as
   elevated ABOVE the rail, not squeezed into it — same loading-
   placeholder technique as .preview-swatch, rendered at the export's
   actual pixel density (see renderCropPreview in export.js) */
.crop-panel[hidden]{display:none}
.crop-panel{position:absolute;top:0;left:0;right:0;background:#fff;border:1px solid var(--line-2);border-radius:10px;box-shadow:0 14px 34px rgba(20,30,45,.24);padding:12px;z-index:5}
.crop-panel-head{display:flex;align-items:flex-start;justify-content:space-between;gap:8px;margin-bottom:8px}
.crop-close{width:22px;height:22px;flex:none;border:none;background:none;color:var(--faint);cursor:pointer;border-radius:5px;display:grid;place-items:center}
.crop-close:hover{background:var(--hover);color:var(--ink)}
.crop-label{font-size:11px;font-weight:650;color:var(--dim)}
.crop-hint{font-weight:400;color:var(--faint)}
.crop-swatch{border:1px solid var(--line-2);border-radius:8px;background:linear-gradient(135deg,#dfe7ee,#c9d6e2) center/cover;aspect-ratio:1/1;position:relative;overflow:hidden}
.crop-swatch::after{content:"";position:absolute;inset:0;background:
  linear-gradient(90deg,transparent 31%,rgba(255,255,255,.5) 32%,transparent 33%),
  linear-gradient(0deg,transparent 61%,rgba(255,255,255,.5) 62%,transparent 63%)}
.crop-swatch.has-image::after{display:none}
/* rendered at a fixed ground span (not a fixed pixel count), so the raw
   render is genuinely low-res at coarse tiers — image-rendering:pixelated
   keeps the browser from smoothing that into a deceptively soft blur */
.crop-swatch img{display:none;width:100%;height:100%;object-fit:fill;image-rendering:pixelated}
.crop-swatch.has-image img{display:block}
.crop-caption{font-size:10.5px;color:var(--faint);margin-top:4px}

.recap{font-size:12px;color:var(--dim);display:flex;flex-direction:column;gap:6px;margin-bottom:14px;padding-bottom:14px;border-bottom:1px solid var(--line-2)}
.recap .row{display:flex;justify-content:space-between;gap:10px}
.recap .row[hidden]{display:none}
.recap .row b{color:var(--ink);font-weight:600;text-align:right}

.summary-price{font-size:22px;font-weight:750;color:var(--ink);margin-bottom:10px}
.summary-price .unit{font-size:12px;font-weight:600;color:var(--dim)}
.split-summary .exp-note{margin-top:0;flex:1}

/* Format/Output as descriptive cards, not pills (2026-07-13) — each option
   is its own card with an icon + one-line caption; selected gets an accent
   border + check, same visual language as .sizecard.sel */
.ctrl-cards{display:grid;grid-template-columns:1fr 1fr;gap:8px}
.ctrl-card{border:1px solid var(--line-2);border-radius:8px;padding:9px 10px;cursor:pointer;background:none;text-align:left;display:flex;gap:9px;align-items:flex-start;position:relative;font:inherit}
.ctrl-card:hover{border-color:var(--accent-line)}
.ctrl-card.on{border-color:var(--accent);background:var(--accent-soft)}
.ctrl-card .ico-wrap{width:26px;height:26px;border-radius:6px;background:var(--line);display:grid;place-items:center;flex:none;color:var(--dim)}
.ctrl-card.on .ico-wrap{background:var(--accent);color:#fff}
.ctrl-card .lbl{display:block;font-size:12.5px;font-weight:650;color:var(--ink)}
.ctrl-card .desc{display:block;font-size:10.5px;color:var(--faint);margin-top:1px}
.ctrl-card .checkmark{position:absolute;top:8px;right:8px;color:var(--accent);display:none}
.ctrl-card.on .checkmark{display:block}
/* Overlay inspector's Marker/Photo Type picker reuses .ctrl-cards verbatim
   (design/markerphoto-option-c-cards.html) but #insp-body is 240px wide, not
   the export dialog's much wider .split-controls — stacked to one column. */
#insp-body .ctrl-cards{grid-template-columns:1fr}
.codetabs{display:flex;gap:4px;padding:12px 16px 0}
.codetab{flex:1;height:30px;padding:0 8px;border-radius:6px;border:none;background:none;color:var(--dim);font-size:12.5px;font-weight:600;cursor:pointer;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.codetab:hover{background:var(--hover)}
.codetab.sel{background:var(--accent-soft);color:var(--accent)}
/* Publish dialog body sizes to its content (no more fixed 52vh box full of
   empty space) — capped + scrollable only as a safety net for a long Stats
   list, not a baseline height every state has to fill. */
.code-body{padding:14px 16px 16px;max-height:70vh;overflow-y:auto}
/* min-height matched to the Stats tab's typical content (tiles + chart +
   two list sections, ~450px) so the dialog doesn't visibly resize switching
   Publish <-> Stats or between not-published/published — shorter states
   (empty-state card, the link card) center in the same box instead of
   leaving it pinned to the top with dead space below; content taller than
   this (a long Stats list) still just grows/scrolls via .code-body above. */
/* fixed height so the dialog doesn't resize between the Links and Analytics
   tabs; taller content (Analytics) scrolls inside instead of growing the box */
.code-hosted{display:flex;flex-direction:column;gap:12px;height:460px;flex:0 1 460px;min-height:0;overflow-y:auto}
.pub-empty{font-size:12px;color:var(--faint);padding:4px 2px}

/* Shared empty-state card — sign-in gate, no-map-yet, not-published-yet,
   basemap-blocked all use this one visual language; only the icon/copy/
   button change, so the tab never looks like four different half-built UIs. */
.pub-emptystate{display:flex;flex-direction:column;align-items:center;text-align:center;gap:6px;padding:22px 14px}
.pub-emptystate .pub-icon{width:40px;height:40px;border-radius:50%;background:var(--accent-soft);color:var(--accent);display:grid;place-items:center;margin-bottom:4px}
.pub-emptystate .pub-icon .ico{width:20px;height:20px}
.pub-emptystate h3{font-size:13.5px;font-weight:650;color:var(--ink)}
.pub-emptystate p{font-size:12px;color:var(--dim);line-height:1.5;max-width:300px}
.pub-emptystate .mini-btn{margin-top:6px}

/* Published: a labeled live-status row (link + copy) and a labeled
   embed-code box (the actual <iframe> snippet, wrapped + copy — not a bare
   "Copy" button hiding what it copies, and grown to fill the dialog's
   height rather than sitting in the same single-line row as the link),
   then Unpublish (left) + Publish (right) sharing one row —
   .pub-publishrow also holds just Publish alone pre-publish, where flex:1
   on a single child is simply full width. */
/* Status row: whether the link (if any) actually matches what's on screen —
   green/amber/gray dot + one line, same spot regardless of which of the
   three states it is so the tab doesn't reshuffle around it. */
.pub-status{display:flex;align-items:center;gap:8px;font-size:12.5px;color:var(--ink)}
.pub-status .dot{width:7px;height:7px;border-radius:50%;flex:none}
.pub-status-working .dot{background:#16a34a}
.pub-status-outdated .dot{background:#d97706}
.pub-status-unpublished .dot{background:var(--faint)}
.pub-section-label{font-size:11px;font-weight:650;color:var(--dim);text-transform:uppercase;letter-spacing:.04em;margin-bottom:-4px}
.pub-linkcard,.share-linkcard,.dshare-linkcard{display:flex;align-items:center;gap:9px;background:var(--canvas);border:1px solid var(--line-2);border-radius:8px;padding:10px 12px;flex:none}
.pub-linkcard a,.share-linkcard a,.dshare-linkcard a{flex:1;min-width:0;font-size:12.5px;font-weight:650;color:var(--ink);text-decoration:none;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pub-linkcard a:hover,.share-linkcard a:hover,.dshare-linkcard a:hover{color:var(--accent);text-decoration:underline}
.pub-linkcard .navicon,.share-linkcard .navicon,.dshare-linkcard .navicon{width:28px;height:28px;flex:none}
.pub-embedbox{flex:1;min-height:80px;display:flex;align-items:flex-start;gap:9px;background:var(--canvas);border:1px solid var(--line-2);border-radius:8px;padding:10px 12px}
.pub-embedbox code{flex:1;min-width:0;font-family:ui-monospace,Consolas,monospace;font-size:11px;line-height:1.6;color:var(--dim);white-space:pre-wrap;word-break:break-all}
.pub-embedbox .navicon{width:28px;height:28px;flex:none}
/* Pinned to the bottom of the scrolling body so it stays reachable when a
   short window forces .code-hosted to scroll. The white background + negative
   margin cover the parent's 12px gap so scrolled content doesn't bleed
   through above it. */
.pub-publishrow{display:flex;gap:8px;flex:none;position:sticky;bottom:0;background:#fff;margin-top:-12px;padding-top:12px}
.pub-publishrow .mini-btn{flex:1}

/* "Publish via Google Maps" checkbox row — plain text row, no card
   container (Aaron's call — it sat oddly boxed next to the un-boxed
   status/label rows around it). Below Business, the row looks IDENTICAL to
   the unlocked one (Aaron: no gray-out, a first cut dimmed it and he asked
   for that removed) — js/codeview.js's googleStyleRow() explains the gate
   via a hover `title` and a tap-triggered MBUI.showLockTip() popover
   instead, not a visual difference on the row itself. */
.pub-googlerow{font-size:12.5px;color:var(--ink);margin-top:2px}
.pub-googlerow label{display:flex;align-items:center;gap:8px;cursor:pointer}
.pub-googlerow input{flex:none}

/* account monthly view usage banner (Stats pane) */
.pub-usage{background:var(--hover);border:1px solid var(--line);border-radius:8px;padding:10px 12px;margin:12px 0}
.pub-usage-top{display:flex;justify-content:space-between;align-items:baseline;font-size:12px;color:var(--dim)}
.pub-usage-top b{font-size:12.5px;font-weight:700;color:var(--ink);font-variant-numeric:tabular-nums}
.pub-usage-bar{height:6px;border-radius:999px;background:var(--line-2);margin-top:7px;overflow:hidden}
.pub-usage-bar i{display:block;height:100%;background:var(--accent);border-radius:999px}
.pub-usage.near .pub-usage-bar i{background:#d97706}
.pub-usage.over .pub-usage-bar i{background:#d33}
.pub-usage-msg{font-size:11.5px;color:var(--dim);margin-top:8px}
.pub-usage-msg a{color:var(--accent);font-weight:600;text-decoration:none}
.pub-usage-msg a:hover{text-decoration:underline}

/* hosted-page stats pane (Stats button on a pubrow) */
.statshead{display:flex;align-items:center;gap:10px}
.statshead-name{font-size:13px;font-weight:650;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;flex:1}
.statshead-range{font-size:11px;color:var(--faint);flex:none}
.stattiles{display:flex;gap:8px}
.stattile{flex:1;background:#fff;border:1px solid var(--line-2);border-radius:8px;padding:10px 12px;display:flex;flex-direction:column;gap:2px}
.stattile b{font-size:20px;font-weight:700;letter-spacing:-.01em}
.stattile span{font-size:11px;color:var(--dim)}
.statsec{background:#fff;border:1px solid var(--line-2);border-radius:8px;padding:10px 12px;display:flex;flex-direction:column;gap:6px}
.statsec-title{font-size:11px;font-weight:650;color:var(--dim);text-transform:uppercase;letter-spacing:.04em}
.statchartwrap{position:relative}
.statchartwrap svg{display:block;width:100%;height:auto}
.stattip{position:absolute;top:-4px;transform:translateX(-50%);background:var(--ink);color:#fff;font-size:11px;line-height:1.3;padding:4px 8px;border-radius:5px;white-space:nowrap;pointer-events:none;z-index:2}
.stattip[hidden]{display:none}
.statrow{display:flex;align-items:center;gap:10px}
.statrow-name{flex:0 0 160px;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.statrow.muted .statrow-name{color:var(--faint);font-style:italic}
.statrow-bar{flex:1;height:8px;display:flex}
.statrow-bar i{display:block;height:100%;background:var(--accent);border-radius:2px;min-width:2px}
.statrow-bar i[hidden]{display:none}
.statrow-n{flex:none;font-size:11.5px;color:var(--dim);min-width:26px;text-align:right;font-variant-numeric:tabular-nums}
/* single column (2026-07-13, was a 2-col grid) — a vertical list reads more
   like a comparison table and matches the print quality list, which reuses
   this same class so both "how big" choices look identical */
.sizegrid{display:grid;grid-template-columns:1fr;gap:8px}
/* the display:grid above would silently defeat the hidden attribute (author
   display beats the UA [hidden] rule) — print mode hides the tier grid */
.sizegrid[hidden]{display:none}
.sizecard{border:1px solid var(--line-2);border-radius:8px;padding:10px 12px;cursor:pointer;background:none;text-align:left;position:relative}
.sizecard:hover{border-color:var(--accent-line)}
.sizecard.sel{border-color:var(--accent);background:var(--accent-soft)}
.sizecard .sz{font-weight:650;font-size:13px}
.sizecard .sub{font-size:11px;color:var(--faint);margin-top:2px}
.sizecard .price{position:absolute;top:9px;right:10px;font-size:11px;font-weight:700;color:var(--accent)}
.sizecard.locked{opacity:.6;cursor:default}
.sizecard.locked .price{color:var(--faint)}
.sizecard.warned .sub{color:#b8860b}
.rec-badge{font-size:9.5px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:#0a7d38;background:rgba(16,163,74,.12);border-radius:4px;padding:1px 5px;margin-left:4px;vertical-align:2px}
/* .seg/.detailrow kept for the design/ mockups that still reference them —
   the live export dialog no longer uses either (Format/Output are cards,
   print quality is a .sizecard list; see the split-dialog rules above) */
.seg{height:28px;padding:0 12px;border:1px solid var(--line-2);background:none;border-radius:999px;font-size:12px;color:var(--dim);cursor:pointer}
.seg.on{background:var(--accent);border-color:var(--accent);color:#fff;font-weight:600}
.coverage{margin-top:10px;font-size:11.5px;color:var(--faint)}
.exp-note{margin-top:10px;font-size:11.5px;color:var(--dim);line-height:1.5}
.exp-note b{color:var(--ink)}
.exp-note:empty{margin-top:0}

/* print panel inside the download dialog (scale + DPI quality) */
#print-panel .fieldlabel{margin-top:14px}
#print-panel .fieldlabel:first-child{margin-top:0}
/* named-scale dropdown above the raw 1:N field — same native-arrow
   treatment as .prop select.dlg-input, full width like the other dialog
   fields */
select.scalepreset{height:34px;appearance:none;-webkit-appearance:none;padding-right:30px;margin-bottom:8px;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236b7480' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 8px center;background-size:14px;background-color:#fff}
.scalefield{display:flex;align-items:center;gap:6px}
.scalefield[hidden]{display:none} /* display:flex would beat the hidden attr */
.scalefield span{font-size:13px;color:var(--dim);flex:none}
.scalefield .dlg-input{width:130px}
/* physical print size shown once here (depends only on Scale) instead of
   repeated on every quality card (DPI just changes resolution/price) */
.printsize-line{font-size:12px;color:var(--dim);margin:10px 0 0}
.printsize-line b{color:var(--ink)}
.printsize-line:empty{display:none}
/* wrapper for the print result card (.sizecard inside, same as the tiers) —
   still used by design/download-option-c-print.html */
.printout{margin-top:12px}
.printout:empty{display:none}

/* step-1 ground-dimension inputs (export nav) */
.exd-dims{display:flex;align-items:center;gap:5px}
.exd-dims input{width:74px;height:28px;border:1px solid var(--line-2);border-radius:6px;padding:0 8px;font:inherit;font-size:12px;outline:none;background:none}
.exd-dims input:focus{border-color:var(--accent-line)}
.exd-dims select{height:28px;border:1px solid var(--line-2);border-radius:6px;font:inherit;font-size:12px;color:var(--dim);background:none;outline:none;padding:0 4px}
.exd-dims .dimx{font-size:12px;color:var(--faint)}

/* my exports dialog */
.exportlist{display:flex;flex-direction:column;gap:6px;max-height:60vh;overflow-y:auto}
.exportrow{display:flex;align-items:center;gap:10px;padding:9px 10px;border:1px solid var(--line-2);border-radius:8px}
.exportrow .ex-info{flex:1;min-width:0}
.exportrow .ex-dim{font-weight:650;font-size:12.5px}
.exportrow .ex-meta{display:block;font-size:11px;color:var(--faint);margin-top:2px}
.exportrow.s-expired,.exportrow.s-failed,.exportrow.s-stale{opacity:.7}
.ex-state{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;color:var(--dim)}
.ex-state.ex-bad{color:#b3261e}
.ex-dl{height:28px;padding:0 12px}
.acct-badge{font-size:9.5px;font-weight:700;color:var(--accent);vertical-align:1px}
.spin{width:12px;height:12px;border-radius:50%;border:2px solid var(--line-2);border-top-color:var(--accent);display:inline-block;animation:spin .7s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}

/* toast */
.toast{position:fixed;bottom:22px;left:50%;transform:translateX(-50%);background:var(--ink);color:#fff;border-radius:8px;padding:10px 18px;font-size:12.5px;z-index:100;box-shadow:0 8px 24px rgba(0,0,0,.25)}

/* maplibre chrome adjustments */
.maplibregl-ctrl-bottom-left{z-index:15}
.maplibregl-ctrl-attrib{font-size:10.5px}

/* ===== overlay editor (2026-07-06) ===== */
.lhead .headbtn{width:24px;height:24px;border:none;background:none;border-radius:6px;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--dim);padding:0}
.lhead .headbtn:hover{background:var(--hover);color:var(--accent)}
/* #btn-language shows the current language's code (e.g. "EN") once one is
   picked, in place of the generic languages icon. A resting background
   (unlike the other plain .headbtn icons) marks it as its own tappable
   control, since text alone next to a section title could read as a label. */
#btn-language{font:700 9.5px/1 var(--font);letter-spacing:-.02em;width:auto;min-width:24px;padding:0 6px;background:var(--hover)}
#btn-language:hover{background:var(--accent-soft);color:var(--accent)}
#overlay-add-menu{top:28px}
#overlay-add-menu button{display:flex;align-items:center;gap:8px}
.ovlist{flex:1;overflow-y:auto;padding:8px}
.ovrow{position:relative;display:flex;align-items:center;gap:8px;padding:7px 8px;border-radius:7px;cursor:pointer;font-size:12.5px}
.ovrow:hover{background:var(--hover)}
/* Show/Hide markers (js/overview.js) */
.ovhidebtn{width:100%}
.ovhidedlg .dlg-body{max-height:60vh;overflow-y:auto}
.ovhide-all{display:flex;gap:14px;margin:8px 0 6px}
.ovhide-all .linkbtn{border:none;background:none;padding:0;color:var(--accent);font:600 12.5px var(--font);cursor:pointer}
.ovhide-section{font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--dim);margin:10px 0 2px}
.ovhide-row{display:flex;align-items:center;gap:10px;padding:8px 2px;border-bottom:1px solid var(--line);cursor:pointer;font-size:13px}
.ovhide-row input{width:16px;height:16px;flex:none;accent-color:var(--accent)}
.ovhide-row span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ovrow .lockmark{display:inline-flex;vertical-align:-2px;color:var(--faint)}
/* Selected rows: square by default so a contiguous run joins into one block;
   only the first and last of a run round off (see renderList's sel-top/sel-bot).
   A single selected row gets both, so it looks exactly as it always did. */
.ovrow.sel{background:var(--accent-soft);color:var(--accent);border-radius:0}
.ovrow.sel.sel-top{border-top-left-radius:7px;border-top-right-radius:7px}
.ovrow.sel.sel-bot{border-bottom-left-radius:7px;border-bottom-right-radius:7px}
.ovrow.drawing{color:var(--dim);font-style:italic;cursor:default}
/* Text/Photo overlay item that won't publish on the current Satellite
   basemap (js/overlay.js SATELLITE_UNSUPPORTED) — dimmed, not disabled: the
   row stays selectable/deletable, .ovrow.sel's own background still wins
   over this so a selected gated row doesn't look unselected. */
.ovrow.ovrow-unsupported{opacity:.55}
.ovrow.ovrow-unsupported.sel{opacity:1}
.ovrow .ovic{color:var(--dim);display:flex;flex:none}
.ovrow.sel .ovic{color:var(--accent)}
.ovrow .ovname{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* always display:flex (not display:none until hover) — reserves the
   button's full height in the row at all times, so a row's height doesn't
   grow on hover (the button, icon+padding, is taller than the plain text
   line) and push every row below it down, which re-triggers hover on the
   shifted target and reads as rows jittering up/down as the cursor moves. */
.ovrow .ovbtn{border:none;background:none;cursor:pointer;color:var(--faint);padding:2px;visibility:hidden;display:flex;flex:none}
.ovrow:hover .ovbtn{visibility:visible}
/* Touch screens: iOS swallows the first tap on any element whose :hover style
   changes what's visible, so the delete button appeared on tap one and the row
   only selected on tap two (Aaron, 2026-09-25). No hover here, so the button is
   simply always shown; the rows are also press-and-drag reorderable. */
@media (hover: none){
  .ovrow .ovbtn{visibility:visible}
  .ovrow{-webkit-touch-callout:none;-webkit-user-select:none;user-select:none}
}
.ovrow .ovbtn:hover{color:var(--accent)}
.ovrow input.ovedit{flex:1;font-size:12.5px;padding:3px 6px;border:1px solid var(--accent);border-radius:5px;min-width:0;font-family:inherit;outline:none}
.ovempty{padding:14px 12px;font-size:12px;color:var(--faint);line-height:1.5}
/* Section: a title in the list, not a drawn shape — no click-select, no map
   icon, bold/uppercase like a group header. No border on the row itself —
   .ovlist-divider (below) is a real standalone rule inserted before it
   instead, skipped when the section is the very first row in the list. */
.ovrow.ovrow-section{font-weight:700;text-transform:uppercase;font-size:11px;letter-spacing:.04em;color:var(--dim);cursor:grab}
.ovrow.ovrow-section .ovic{display:none}
.ovlist-divider{height:1px;background:var(--line);margin:8px 6px}
/* list drag-to-reorder (every row, sections included). Rather than
   highlighting the row being dragged over (reads as "drop into this row"),
   a thin bar between rows shows exactly where the dragged row will land —
   above the target when the cursor is over its top half, below when over
   its bottom half. */
.ovrow.dragging{opacity:.4}
.ovrow.drop-before::before,
.ovrow.drop-after::after{content:'';position:absolute;left:6px;right:6px;height:2px;background:var(--accent);border-radius:1px;pointer-events:none}
.ovrow.drop-before::before{top:-1px}
.ovrow.drop-after::after{bottom:-1px}
.rangefield{flex:1;height:32px;display:flex;align-items:center;gap:8px;min-width:0}
.rangefield input[type=range]{flex:1;min-width:0}
/* Dimensions section (js/overlay.js dimensionSection) — .dimnum is a plain
   .proptext number input, needs no extra rule of its own; .propreadout is
   the read-only counterpart (Area/Length/Side length totals) sitting in the
   same .prop label+value slot other rows put an input in. */
.propreadout{flex:1;text-align:right;font-size:12.5px;color:var(--ink);font-weight:600;font-variant-numeric:tabular-nums}
/* Appearance-tab checkbox row (js/overlay.js propCheckRow — currently the
   photo overlay's "Lock aspect ratio"). Its label reads left-to-right after
   the box rather than sitting in the fixed 44px .plabel column: these labels
   are sentences, not the one-word field names that column is sized for. */
.prop.propcheck{cursor:pointer;user-select:none}
.prop.propcheck input{margin:0;accent-color:var(--accent);cursor:pointer;flex:none}
.prop.propcheck .pchecklabel{font-size:12px;color:var(--dim)}
/* Overlay-tab's own rangeRow (Weight/Sides/Rotation/Size) still nests its
   readout inside .rangefield, so this descendant rule keeps styling it. */
.rangefield .rv{font-size:11.5px;color:var(--dim);font-variant-numeric:tabular-nums;width:38px;text-align:right;flex:none}
/* Multi-select inspector (js/overlay.js rangeRow's mixed param, 2026-08-08):
   selected items disagree on this field — same faded-italic convention as
   .propcolor.mixed .hex above. */
.rangefield .rv.mixed{color:var(--faint);font-style:italic}
/* Layers-tab Opacity/Width rows (js/ui.js) instead render .rv as a sibling
   of .rangefield, not nested inside it — same fixed-width-trailing-column
   trick as .propaccessory above, so input[type=range] — the part that
   actually reads as "the slider" — renders the exact same width as
   .colorfield, and the two trailing columns (the eye icon, the %/px
   readout) land flush on the same right edge across every row. */
.rv{font-size:11.5px;color:var(--dim);font-variant-numeric:tabular-nums;width:36px;text-align:right;flex:none}

/* ---------- range sliders (shared: Layers/Overlay dimension + Opacity knobs,
   top-nav detail-zoom, export-flow detail-zoom) — a plain accent-colored
   native thumb sits on an accent-colored filled track and visually vanishes
   into it, so both are re-skinned: filled portion in --accent, thumb a white
   disc with an --accent ring (same look as the color picker's own hue/SL
   dots) so it reads as a distinct handle at any position. --pct (0-100%,
   set from JS on render + on input) draws the fill boundary. */
input[type=range]{-webkit-appearance:none;appearance:none;width:100%;height:16px;margin:0;background:none;cursor:pointer}
input[type=range]::-webkit-slider-runnable-track{height:4px;border-radius:999px;background:linear-gradient(to right,var(--accent) 0 var(--pct,0%),var(--line-2) var(--pct,0%) 100%)}
input[type=range]::-moz-range-track{height:4px;border-radius:999px;background:var(--line-2)}
input[type=range]::-moz-range-progress{height:4px;border-radius:999px 0 0 999px;background:var(--accent)}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;box-sizing:border-box;margin-top:-6px;width:16px;height:16px;border-radius:50%;background:#fff;border:2px solid var(--accent);box-shadow:0 1px 3px rgba(15,25,40,.35)}
input[type=range]::-moz-range-thumb{box-sizing:border-box;width:16px;height:16px;border-radius:50%;background:#fff;border:2px solid var(--accent);box-shadow:0 1px 3px rgba(15,25,40,.35)}

/* Property panel's Opacity and Width sliders re-skin the shared range
   control in --faint instead of --accent — neither is a selection control
   (Opacity's a transparency knob, Width's a stroke-thickness knob), so
   neither should read as an active/interactive accent color. prop-muted is
   the same treatment for every Overlay-tab slider (js/overlay.js's
   rangeRow) — Weight/Opacity/Size/Rotation/Sides are all dimension knobs
   too, so they match the Layers tab instead of standing out in accent. */
.prop-opacity input[type=range]::-webkit-slider-runnable-track,.prop-width input[type=range]::-webkit-slider-runnable-track,.prop-muted input[type=range]::-webkit-slider-runnable-track{background:linear-gradient(to right,var(--faint) 0 var(--pct,0%),var(--line-2) var(--pct,0%) 100%)}
.prop-opacity input[type=range]::-moz-range-progress,.prop-width input[type=range]::-moz-range-progress,.prop-muted input[type=range]::-moz-range-progress{background:var(--faint)}
.prop-opacity input[type=range]::-webkit-slider-thumb,.prop-width input[type=range]::-webkit-slider-thumb,.prop-muted input[type=range]::-webkit-slider-thumb{border-color:var(--faint)}
.prop-opacity input[type=range]::-moz-range-thumb,.prop-width input[type=range]::-moz-range-thumb,.prop-muted input[type=range]::-moz-range-thumb{border-color:var(--faint)}

/* Buildings' Stroke width when Render isn't "Outline" — inert but tappable
   (see js/ui.js widthProp's disabledReason): the slider itself gets
   pointer-events:none so dragging/clicking it does nothing, while a
   transparent overlay sibling sits on top and catches the tap to explain
   why via toast. A real `disabled` attribute would swallow that tap
   silently instead. Visually it stays the same regular slider look (same
   track/thumb, no opacity wash) just recolored a step lighter than the
   already-light --faint regular slider — --line-2/--line instead — and its
   label/readout drop to --faint, so "disabled" reads as a lighter color
   rather than a translucent film over the same one. */
.prop-disabled .rangefield{position:relative}
.prop-disabled .rangefield input[type=range]{pointer-events:none}
.prop-disabled .plabel{color:var(--faint)}
.prop-disabled .rv{color:var(--faint)}
.prop-disabled input[type=range]::-webkit-slider-runnable-track{background:linear-gradient(to right,var(--line-2) 0 var(--pct,0%),var(--line) var(--pct,0%) 100%)}
.prop-disabled input[type=range]::-moz-range-track{background:var(--line)}
.prop-disabled input[type=range]::-moz-range-progress{background:var(--line-2)}
.prop-disabled input[type=range]::-webkit-slider-thumb{border-color:var(--line-2)}
.prop-disabled input[type=range]::-moz-range-thumb{border-color:var(--line-2)}
.width-disabled-overlay{position:absolute;inset:0;background:none;border:none;padding:0;margin:0;cursor:not-allowed}
.insp-hint{padding:12px 16px;font-size:11.5px;color:var(--faint);line-height:1.5}

/* marker icon picker (overlay inspector) */
.prop.iconprop{align-items:flex-start}
.prop.iconprop .plabel{padding-top:5px}
/* Wraps the search input + its dropdown grid. */
.iconpickwrap{flex:1;min-width:0;display:flex;flex-direction:column;gap:6px}
.iconpickwrap .iconsearch{flex:none}
/* Leading glyph showing the currently-selected icon inside the search field
   itself (Aaron's ask) — a plain <input> can't hold an inline SVG, so this
   sits absolutely over its left edge and the input gets left padding to
   clear it. Sized/centered like a .ipbtn's own glyph, just smaller. */
.iconsearchfield{position:relative}
/* width:100% (Aaron's report — narrower than every other field in the
   column): .iconsearch used to be a direct flex child of .iconpickwrap
   (flex-direction:column, default align-items:stretch), which is what
   made it fill the row's width with no rule of its own. Wrapping it in
   this non-flex .iconsearchfield for the glyph above dropped that free
   stretch — box-sizing:border-box so the padding-left below doesn't grow
   it past 100%. */
.iconsearchfield .iconsearch{box-sizing:border-box;width:100%;padding-left:32px}
/* No glyph (freshly cleared on focus — js/importcsv.js/js/overlay.js) means
   nothing reserves that space, so the text/placeholder shouldn't start
   indented past it either — same left padding every other .proptext gets. */
.iconsearchfield:has(.iconsearch-glyph:empty) .iconsearch{padding-left:9px}
.iconsearch-glyph{position:absolute;left:8px;top:50%;transform:translateY(-50%);
  width:18px;height:18px;display:flex;align-items:center;justify-content:center;color:var(--ink);pointer-events:none}
.iconsearch-glyph svg{width:16px;height:16px;fill:currentColor}
.iconsearch-glyph .ipdot{width:12px;height:12px;border-radius:50%;background:currentColor;display:block}
.iconsearch-glyph .ipnum{position:relative;display:flex;width:18px;height:18px}
.iconsearch-glyph .ipnum svg{width:18px;height:18px}
.iconsearch-glyph .ipnum b{position:absolute;inset:0 0 3px 0;display:flex;align-items:center;justify-content:center;font-size:8px;font-weight:700;color:#fff}
.iconsearch-glyph:empty{display:none}
/* The grid used to sit inline here, always visible under the search box —
   325 icons made that a lot of permanent dead space (Aaron: "we dont need
   to show the icons on the screen"). Now a dropdown, shown on focusing the
   search input — .mp-menu (position:fixed, same reasoning as the Address/
   Details dropdowns above: .col-setup/.col-data's own overflow-y:auto would
   otherwise clip it) supplies the popover chrome, .iconpick keeps its own grid layout
   on top of that. Coordinates come from the input's rect at open time (see
   placeIconMenu() in js/importcsv.js). */
.iconpick{display:grid;grid-template-columns:repeat(5,1fr);gap:4px;max-height:190px;overflow-y:auto;align-content:start;padding:4px}
.iconpick .ipbtn{height:28px;border:1px solid var(--line-2);border-radius:6px;background:none;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--dim);padding:0}
/* [hidden] needs its own rule: the plain .ipbtn rule's display:flex (two
   classes, specificity 0,2,0) otherwise beats the UA stylesheet's
   [hidden]{display:none} (0,1,0) — the search filter was setting el.hidden
   correctly and the button just stayed visible anyway. Caught by actually
   screenshotting a live search, not by the DOM check alone (b.hidden read
   true the whole time). */
.iconpick .ipbtn[hidden]{display:none}
.iconpick .ipbtn:hover{border-color:var(--accent-line);color:var(--accent)}
.iconpick .ipbtn.on{border-color:var(--accent);background:var(--accent-soft);color:var(--accent)}
.iconpick .ipbtn svg{width:16px;height:16px}
.iconpick .ipdot{width:12px;height:12px;border-radius:50%;background:currentColor;box-shadow:0 0 0 2px #fff,0 0 0 3px currentColor}
/* Numbered-pin tile: the pin-outline svg (fills currentColor same as any
   other icon tile) with a plain "1" placeholder overlaid near the pin's own
   head circle — a representative preview, not a live count (that only
   exists once a real item is placed in a section, js/overlay.js's
   sectionOrderOf). */
.iconpick .ipnum{position:relative;display:flex;width:16px;height:16px}
.iconpick .ipnum svg{width:16px;height:16px}
.iconpick .ipnum b{position:absolute;top:44%;left:50%;transform:translate(-50%,-50%);font:700 7px/1 system-ui,sans-serif;color:#fff;pointer-events:none}
/* Shown in place of the grid while js/overlay-icons.js is being fetched
   (2026-08-04, on-demand load) — same box in both places it's used: the
   inspector's freshly-built .prop.iconprop, and the CSV dialog's static
   .iconpick grid, so no flex/grid context to lean on either way. */
.iconpick-loading{padding:10px 0;font-size:12.5px;color:var(--dim)}

/* marker Title + Location fields (Marker section in overlay inspector) */
.proptext{flex:1;min-width:0;height:32px;border:1px solid var(--line-2);border-radius:6px;padding:0 9px;font:inherit;font-size:12.5px;outline:none}
.proptext:focus{border-color:var(--accent-line)}
.proplocation{align-items:flex-start}
.proplocation .plabel{padding-top:7px}
.locfield{flex:1;min-width:0;position:relative}
/* Overlay inspector's shared Location section (js/overlay.js
   locationSearchRow): .locfield sits directly in a titled .section now, not
   inside a .prop flex row next to a side label, so the input needs its own
   full-width rule instead of relying on the row's flex:1. */
.section > .locfield .proptext{width:100%}
.locresults{position:fixed;background:#fff;border:1px solid var(--line-2);border-radius:6px;max-height:180px;overflow:auto;z-index:20;box-shadow:0 4px 12px rgba(20,30,45,.12)}
.locresults .locsearching{padding:7px 9px;font-size:12px;color:var(--faint)}
.locresults button{display:block;width:100%;text-align:left;border:none;background:none;padding:7px 9px;font:inherit;font-size:12px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.locresults button:hover{background:var(--accent-soft)}
.locresults .sub{display:block;font-size:10.5px;color:var(--faint);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}

/* Route stops (js/overlay.js routeStopsSection) — the Location section's list
   of one search field per node, in route order. Row layout is grip + number +
   field + remove, and the field itself is the same .locfield the Marker
   Location section uses, so its dropdown positioning is already handled. The
   drag feedback deliberately matches .ovrow's (the overlay list), since it's
   the same gesture doing the same thing to a different list. */
.stoplist{display:flex;flex-direction:column;gap:6px}
.stoprow{position:relative;display:flex;align-items:center;gap:6px}
.stoprow.dragging{opacity:.4}
.stoprow.drop-before::before,
.stoprow.drop-after::after{content:'';position:absolute;left:0;right:0;height:2px;background:var(--accent);border-radius:1px;pointer-events:none}
.stoprow.drop-before::before{top:-4px}
.stoprow.drop-after::after{bottom:-4px}
.stopgrip{flex:none;display:grid;place-items:center;width:14px;color:var(--faint);cursor:grab}
.stoprow.dragging .stopgrip{cursor:grabbing}
.stopnum{flex:none;width:18px;height:18px;border-radius:50%;display:grid;place-items:center;background:var(--hover);color:var(--dim);font-size:10.5px;font-weight:700}
.stoprow .locfield{flex:1;min-width:0}
.stoprow .proptext{width:100%}
.stopdel{flex:none;display:grid;place-items:center;width:24px;height:24px;border:none;background:none;border-radius:5px;color:var(--faint);cursor:pointer}
.stopdel:hover:not(:disabled){color:var(--ink);background:var(--hover)}
.stopdel:disabled{opacity:.35;cursor:default}
.stopadd{margin-top:8px;display:flex;align-items:center;gap:6px;border:1px dashed var(--line-2);background:none;border-radius:6px;padding:6px 9px;width:100%;font:inherit;font-size:12px;font-weight:600;color:var(--dim);cursor:pointer}
.stopadd:hover{border-color:var(--accent-line);color:var(--ink)}

/* text overlay item (Text section in overlay inspector) */
.ovtextarea{flex:1;min-width:0;height:56px;resize:vertical;border:1px solid var(--line-2);border-radius:6px;padding:6px 8px;font:inherit;font-size:12.5px;outline:none}
.ovtextarea:focus{border-color:var(--accent-line)}

.ovtogglegroup{flex:1;display:flex;gap:6px}
.ovtogglebtn{height:28px;flex:1;border:1px solid var(--line-2);border-radius:6px;background:none;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--dim)}
.ovtogglebtn:hover{border-color:var(--accent-line);color:var(--accent)}
.ovtogglebtn.on{border-color:var(--accent);background:var(--accent-soft);color:var(--accent)}

/* published-map overview panel settings (js/overview.js) — map-level, not
   per-shape, so it skips .section's bordered-group treatment and just flows
   in the panel body: an intro line, a plain show/hide row, then full-width
   stacked (label-above) fields, since label-beside .prop rows are cramped
   for a photo + two text fields in a 240px column. .ov-panel stays specific
   to this settings panel, but .ovfield/.ovphotozone/.ovimg-iconbtn below are
   shared with the per-shape inspector's Popup tab (.popup-fields further
   down) — same field/dropzone look, different owner — and as of 2026-08-17
   so are .ov-showrow/.ovswitch/.ov-fields.disabled: the Popup tab and the
   CSV import modal both lead with the same "Enable popup" pill switch over
   the same graying-out field block, which is the identical job this row does
   here. Change one, check all three. */
.ov-panel{padding:14px 16px 16px}
.ov-panel .insp-hint{padding:0 0 14px}
.ov-showrow{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:0 0 12px;border-bottom:1px solid var(--line);margin-bottom:14px}
.ov-showrow span{font-size:12.5px;color:var(--ink)}
.ovswitch{position:relative;width:34px;height:19px;border-radius:999px;background:var(--line-2);border:none;cursor:pointer;flex:none;transition:background .15s}
.ovswitch::after{content:'';position:absolute;top:2px;left:2px;width:15px;height:15px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);transition:transform .15s}
.ovswitch.on{background:var(--accent)}
.ovswitch.on::after{transform:translateX(15px)}
/* grays out + stops accepting input while the Show switch is off — same
   convention as .tree.disabled/.presetlist.disabled elsewhere */
.ov-fields.disabled{opacity:.45;pointer-events:none}
.ovfield{margin-bottom:12px}
.ovfield label{display:block;font-size:11px;font-weight:600;color:var(--dim);margin-bottom:5px}
/* Hairline between the Overview panel's field groups (js/overview.js
   groupDivider): Layout + List marker / Photos / Title + Description /
   Categories. margin-top is small because the .ovfield above already carries
   12px of its own. */
.ov-divider{height:1px;background:var(--line);margin:4px 0 16px}
/* Popup tab CTA checkboxes (js/overlay.js ctaCheckRow) — plain native
   checkbox, not the pill .ovswitch above: this gates which of several
   independent buttons show, not one on/off field group. */
.ovcheck{display:flex;align-items:center;gap:7px;margin-bottom:12px;font-size:12.5px;color:var(--ink);cursor:pointer;user-select:none}
.ovcheck input{margin:0;accent-color:var(--accent);cursor:pointer;flex:none}
/* Link CTA's own input row (js/overlay.js popupPane) sits right under its
   .ovcheck, which already reads "Link" — the field's own <label> stays in
   the DOM (05c2's smoke check looks it up by text) but is hidden here
   rather than showing the word twice in a row; margin pulled up to close
   the gap the hidden label's own margin-bottom would otherwise leave. */
.ovfield.cta-subfield{margin-top:-8px}
.ovfield.cta-subfield label{display:none}
.ovfield input, .ovfield textarea, .ovfield select{width:100%;border:1px solid var(--line-2);border-radius:6px;padding:7px 9px;font:inherit;font-size:12.5px;outline:none}
.ovfield input:focus, .ovfield textarea:focus, .ovfield select:focus{border-color:var(--accent-line)}
.ovfield textarea{resize:vertical;min-height:56px}
/* Crop preset dropdown (js/overlay.js popupPhotoRatioRow) — same flush-right
   native-arrow treatment as .prop select.dlg-input below, just scoped to
   this tab's .ovfield convention instead. */
.ovfield select{height:32px;appearance:none;-webkit-appearance:none;padding-right:30px;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236b7480' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 8px center;background-size:14px}
/* ---------- tags + tag categories ----------
   Per-shape tags (js/overlay.js tagsSection) and the Overview panel's
   categories (js/overview.js categoriesRow) share the chip row and the
   MBUI.tagPicker dropdown below, which is why these live in one block rather
   than under either panel's own section. */
.tagchips{display:flex;flex-wrap:wrap;gap:5px;margin-bottom:8px}
.tagchips:empty{display:none} /* no chips, no gap — the "Add tags" button sits flush under the section head */
.tagchip{display:inline-flex;align-items:center;gap:2px;max-width:100%;border:1px solid var(--line-2);background:var(--hover);border-radius:999px;padding:2px 3px 2px 9px;font-size:11.5px;line-height:1.5;color:var(--ink)}
.tagchip > span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* Multi-selection where only SOME selected shapes carry this tag — the
   dashed edge is the chip-sized equivalent of the picker's indeterminate
   checkbox, not a separate state. */
.tagchip.partial{border-style:dashed;color:var(--dim)}
/* Double-click a chip's name to rename the tag map-wide (js/overlay.js
   editChip). The pill stays the pill: the label is swapped for a field sized
   to its own content, so the chip grows with what is typed instead of
   snapping to a fixed-width input that reads as a different control. */
.tagchip-name{cursor:text}
.tagchip.editing{border-color:var(--accent-line);background:var(--panel)}
.tagchip-edit{border:none;background:none;padding:0;margin:0;font:inherit;color:var(--ink);
  min-width:30px;max-width:150px;outline:none}
.tagchip-x{flex:none;width:16px;height:16px;border:none;background:none;padding:0;border-radius:50%;display:grid;place-items:center;color:var(--faint);cursor:pointer}
.tagchip-x:hover{background:var(--line-2);color:var(--ink)}
.tagchip-x svg{width:11px;height:11px}
/* Expands inline, not as a floating popover — see MBUI.tagPicker's own
   comment: the inspector rail scrolls and clips, so an absolutely positioned
   panel gets cut off near the bottom of a long panel. */
.tagpick-btn{display:flex;align-items:center;justify-content:center;gap:5px;width:100%;height:30px;border:1px dashed var(--line-2);border-radius:6px;background:none;font:inherit;font-size:12px;color:var(--dim);cursor:pointer}
.tagpick-btn:hover{border-color:var(--accent-line);color:var(--accent)}
.tagpick-btn.on{border-style:solid;border-color:var(--accent);background:var(--accent-soft);color:var(--accent)}
.tagpick-btn svg{width:13px;height:13px}
.tagpick-pop{margin-top:6px;padding:7px;border:1px solid var(--line-2);border-radius:7px;background:var(--panel);box-shadow:0 4px 14px rgba(15,25,40,.10)}
.tagpick-input{width:100%;height:28px;border:1px solid var(--line-2);border-radius:6px;padding:0 8px;font:inherit;font-size:12.5px;outline:none}
.tagpick-input:focus{border-color:var(--accent-line)}
.tagpick-list{max-height:150px;overflow-y:auto;margin-top:6px}
.tagpick-create{display:block;width:100%;margin-bottom:4px;padding:6px 8px;text-align:left;border:none;border-radius:5px;background:var(--accent-soft);color:var(--accent);font:inherit;font-size:12px;font-weight:600;cursor:pointer}
.tagpick-create:hover{background:rgba(45,127,249,.18)}
/* label.tagpick-row, not .tagpick-row: inside the Overview panel this sits in
   an .ovfield, whose `label{display:block;font:600 11px;margin-bottom:5px}`
   rule would otherwise out-specify a bare class selector and flatten the row
   into a block of small grey text with the checkbox jammed against it. */
label.tagpick-row{display:flex;align-items:center;gap:7px;margin:0;padding:4px;border-radius:5px;font-size:12.5px;font-weight:400;color:var(--ink);cursor:pointer;user-select:none}
.tagpick-row:hover{background:var(--hover)}
/* width:auto is not redundant — inside the Overview panel the picker sits in
   an .ovfield, whose own `input{width:100%}` would stretch the checkbox
   across the row and wrap the tag name onto a line of its own. */
.tagpick-row input{width:auto;margin:0;accent-color:var(--accent);cursor:pointer;flex:none}
.tagpick-row span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.tagpick-empty{padding:6px 4px;font-size:11.5px;color:var(--faint)}
/* Category editor (Overview panel settings). One bordered card per category:
   name + delete on the head row, its chosen tags as chips, its own picker
   below. */
/* Each category is a plain block in the flow, separated from the next by a
   hairline (Aaron's call, 2026-09-04) — a bordered card per category read as
   a second level of container inside an already-boxed panel. The rule sits
   BETWEEN categories only (`+`), never above the first or below the last, so
   it reads as a separator rather than an outline. */
.ovcatlist{display:flex;flex-direction:column}
.ovcatlist:empty{display:none}
.ovcat{padding:10px 0}
.ovcat:first-child{padding-top:0}
.ovcat + .ovcat{border-top:1px solid var(--line)}
.ovcat-head{display:flex;align-items:center;gap:6px;margin-bottom:7px}
.ovfield .ovcat-head input{flex:1;width:auto;min-width:0} /* .ovfield input's own width:100% would push the delete button off the row */
.ovcat-del{flex:none;width:26px;height:26px;border:none;background:none;padding:0;border-radius:5px;display:grid;place-items:center;color:var(--faint);cursor:pointer}
.ovcat-del:hover{background:var(--hover);color:var(--ink)}
.ovcat-del svg{width:14px;height:14px}
.ovcats .ovcat-add{display:flex;align-items:center;justify-content:center;gap:5px;width:100%;margin-top:4px}
.ovcats .ovcat-add[hidden]{display:none} /* display:flex above beats the UA [hidden] rule — same trap .ovphotozone[hidden] hit */
.ovcats .ovcat-add svg{width:13px;height:13px}
.ovcat-hint{margin-top:7px;font-size:11px;color:var(--faint);line-height:1.5}
.ovphotozone{position:relative;width:100%;height:100px;border-radius:6px;border:1px dashed var(--line-2);background:var(--hover);display:flex;align-items:center;justify-content:center;overflow:hidden;cursor:pointer}
.ovphotozone.has-img{border-style:solid;cursor:default}
.ovphotozone[hidden]{display:none} /* display:flex above would beat the hidden attr — the gallery hides the zone once it has photos */
/* Drag-and-drop hover state (2026-08-10), same treatment as .dropzone.dragover
   above — only ever added while the zone is still empty (js/overlay.js's
   popupPhotoRow()/js/overview.js's photoRow() gate dragover the same way
   the click handler gates opening the file picker), so no has-img override
   is needed here. */
.ovphotozone.dragover{border-color:var(--accent);background:var(--accent-soft)}
.ovphotozone-empty{display:flex;flex-direction:row;align-items:center;gap:12px;color:var(--faint);font-size:10.5px;pointer-events:none}
.ovphotozone.has-img .ovphotozone-empty{display:none}
/* Upload photo / Photo URL, side by side — the zone itself stays the
   "Upload photo" click target (pointer-events:none here, falls through to
   .ovphotozone's own listener), .ovpz-url opts back in as its own
   independently-clickable target. Icon/text/gap/divider all sized down
   together (design/photourl-option-k-smallicons.html, Aaron's pick after
   4 rounds of iteration — same structure as always, just smaller) so the
   pair doesn't look oversized/washed-out floating in the dashed zone. */
.ovpz-opt{display:flex;flex-direction:column;align-items:center;gap:6px}
.ovpz-opt .ico{width:14px;height:14px}
.ovpz-url{pointer-events:auto;cursor:pointer;border-radius:6px;padding:4px 6px;margin:-4px -6px}
.ovpz-url:hover{color:var(--accent)}
.ovpz-sep{width:1px;height:26px;background:var(--line-2);flex:none}
/* Rendered below .ovphotozone, not inside it (that box clips overflow for
   the preview image, which would clip this too) — a real dialog would need
   its own scrim/positioning plumbing for one input field, so this is a
   plain reveal-in-place row instead, closed by Escape, Enter (submits), or
   an outside click. */
.ovpz-urlrow{display:flex;gap:6px;margin-top:8px}
.ovpz-urlrow[hidden]{display:none} /* display:flex above would beat the hidden attr */
.ovpz-urlrow .ovpz-urlinput{flex:1;min-width:0}
.ovphotozone img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.ovphotozone .ovimg-actions{position:absolute;top:6px;right:6px;display:flex;gap:6px}
.ovimg-iconbtn{width:26px;height:26px;border-radius:50%;border:none;background:rgba(255,255,255,.92);color:var(--ink);
  display:flex;align-items:center;justify-content:center;padding:0;cursor:pointer;box-shadow:0 1px 3px rgba(15,25,40,.25)}
.ovimg-iconbtn:hover{background:#fff;color:var(--accent)}
.ovimg-iconbtn[hidden]{display:none} /* display:flex above would beat the hidden attr */
.ovimg-status{font-size:11px;color:var(--dim)}
.ovimg-status:empty{display:none}
.ovphotozone .ovimg-status{position:absolute;left:6px;bottom:6px;background:rgba(255,255,255,.9);padding:2px 6px;border-radius:4px}
/* Photo gallery (MBUI.photoGallery, js/ui.js — 2026-08-18). Shown INSTEAD of
   the .ovphotozone dropzone above once there's at least one photo; the zone
   comes back when the last one is removed, so an empty gallery looks exactly
   like the single-photo field it replaced. Three per row at the 240px rail
   width, which keeps a full 6-photo gallery to two tidy rows.
   Layout follows design/photogallery-option-a-coverbadge.html (Aaron's pick,
   2026-08-19): Remove is the only per-thumbnail button, order is changed by
   dragging the thumbnail, and there is no number/cover badge. */
.ovgallery{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;border-radius:6px}
.ovgallery.dragover{outline:1px dashed var(--accent);outline-offset:3px;background:var(--accent-soft)}
.ovgallery[hidden]{display:none} /* display:grid above would beat the hidden attr */
.ovgthumb{position:relative;aspect-ratio:1/1;border-radius:6px;overflow:hidden;border:1px solid var(--line-2);
  background:var(--hover);cursor:grab}
.ovgthumb img{width:100%;height:100%;object-fit:cover;display:block}
/* Reorder feedback: the tile being dragged fades, and the tile it would land
   on gets an accent ring so the destination is unambiguous before releasing. */
.ovgthumb.dragging{opacity:.4;cursor:grabbing}
.ovgthumb.dropinto{outline:2px solid var(--accent);outline-offset:-2px}
/* Remove stays hidden until hover/focus — at this tile size a button sitting
   permanently on the photo leaves too little of it visible to recognise. */
.ovgthumb-actions{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;gap:6px;
  background:rgba(15,25,40,.45);opacity:0;transition:opacity .12s}
.ovgthumb:hover .ovgthumb-actions,.ovgthumb:focus-within .ovgthumb-actions{opacity:1}
.ovgthumb.dragging .ovgthumb-actions{opacity:0} /* the cursor is over it the whole drag */
.ovgthumb-actions .ovimg-iconbtn{width:22px;height:22px}
/* "Add more" tile, split in two: upload on the left, Photo URL on the right —
   the same two options, in the same order, as the empty dropzone above.
   Icon-only, since a tile this size can't carry labels legibly. */
.ovgadd-split{aspect-ratio:1/1;border-radius:6px;border:1px dashed var(--line-2);background:var(--hover);
  display:flex;flex-direction:row;align-items:center;overflow:hidden}
.ovgadd-half{align-self:stretch;flex:1;display:flex;align-items:center;justify-content:center;
  color:var(--faint);cursor:pointer}
.ovgadd-half:hover{color:var(--accent);background:var(--accent-soft)}
.ovgadd-half .ico{width:14px;height:14px}
.ovgadd-sep{width:1px;height:14px;background:var(--line-2);flex:none}

/* per-shape inspector's Popup tab (Title/Details/Photo/Link — js/overlay.js
   popupPane()) — reuses the Overview settings panel's field/dropzone classes
   above verbatim (2026-07-28, Aaron's call, design/shapeinfo-*.html explored
   the options), just its own padding wrapper since it has no show/hide
   switch or .ov-fields.disabled state to support. */
.popup-fields{padding:14px 16px 16px}
.popup-fields .insp-hint{padding:0 0 14px}
/* Photo/Aspect ratio/Size/Details field labels styled like the Appearance
   tab's own section titles (.sec-top .st) — uppercase, bold, tracked out —
   rather than the plain .ovfield label these share with the Overview
   settings panel (Aaron's ask: match Appearance). Scoped to this tab only,
   not a blanket .ovfield change, so the Overview panel's own labels are
   untouched. */
.popup-fields .ovfield label{text-transform:uppercase;letter-spacing:.07em;font-weight:700}

/* Popup tab's live ON-MAP preview (js/overlay.js showPreviewPopup/
   buildPreviewPopupCard) — Aaron's call: "the same as the real published
   map," a real maplibregl.Popup on the editor's own live map, not a panel
   mockup. KEEP IN SYNC: this block duplicates pub.html's .mb-embed-popup
   rules — same class names (mb-popup-body/img/title/desc/actions/btn) and
   the same real card widths (300px default, 220 Small, 380 Large) — EXCEPT
   the image cap, deliberately shrunk well below pub.html's own 170/120/220:
   Aaron's report, a photo at the real published size dominated the editor's
   much smaller map viewport in a way it never would on a full-page
   published site. Scoped under its own mb-editor-popup class (the
   maplibregl.Popup's className, set in showPreviewPopup) rather than
   reusing mb-embed-popup itself — a deliberately distinct hook so this is
   never accidentally targeted by anything meant for the published page.
   `width`, NOT `max-width` (2026-08-13): the card holds .mb-popup-img at
   width:100%, and a percentage width contributes nothing to a shrink-to-fit
   box's intrinsic size — so a title+photo card collapsed to the title's
   width and never reached a max-width cap, leaving the Size preset visibly
   changing only the image's height. Requires maxWidth:'none' on the Popup
   (js/overlay.js showPreviewPopup): maplibre's own 240px default is an
   INLINE max-width on the outer .maplibregl-popup, which otherwise clamps
   Medium and Large to the same 240px. The max-width that remains is a
   narrow-viewport guard, not the sizing mechanism: with that 240px cap
   lifted, nothing else would stop a 380px Large card from overflowing a
   phone-width screen. */
.mb-editor-popup .maplibregl-popup-content{font:13px/1.45 system-ui,sans-serif;padding:10px 12px;width:300px;max-width:calc(100vw - 24px)}
.mb-editor-popup.mb-popup-sm .maplibregl-popup-content{width:220px}
.mb-editor-popup.mb-popup-lg .maplibregl-popup-content{width:380px}
.mb-editor-popup .mb-popup-body{color:var(--ink)}
/* max-height here is a safety ceiling only (2026-08-13, matches pub.html's
   own switch to Original-by-default) — deliberately smaller than pub.html's
   real numbers below it (same ~0.6x scale-down as before this change),
   still so a photo at true published size doesn't dominate the editor's
   much smaller map viewport (2026-08-12 rationale, unchanged). */
.mb-editor-popup .mb-popup-img{display:block;width:100%;height:auto;max-height:250px;object-fit:cover;border-radius:4px;margin:0 0 6px}
.mb-editor-popup.mb-popup-sm .mb-popup-img{max-height:180px}
.mb-editor-popup.mb-popup-lg .mb-popup-img{max-height:320px}
/* Crop preset (js/overlay.js popupPhotoRatioRow/POPUP_RATIOS) — KEEP IN SYNC
   with pub.html's own .mb-ratio-* rules. */
.mb-editor-popup .mb-popup-img.mb-ratio-square{aspect-ratio:1/1}
.mb-editor-popup .mb-popup-img.mb-ratio-4x3{aspect-ratio:4/3}
.mb-editor-popup .mb-popup-img.mb-ratio-3x2{aspect-ratio:3/2}
.mb-editor-popup .mb-popup-img.mb-ratio-16x9{aspect-ratio:16/9}
/* Photo fill = Fit (js/overlay/popup.js popupPhotoFitRow) — KEEP IN SYNC with pub.html. */
.mb-editor-popup .mb-popup-img.mb-fit-contain{object-fit:contain;background:#f1f3f5}
/* Gallery cue in the editor preview (2026-08-18): photo #1 with the published
   carousel's dots and a count, NOT a working carousel — buildPreviewPopupCard()
   in js/overlay.js explains why the interaction deliberately lives only in
   pub.html. The dots are positioned relative to .mb-popup-body rather than the
   image, since there's no carousel box here to anchor to; the image is always
   the first child, so bottom-anchoring to it isn't possible without one. */
.mb-editor-popup .mb-popup-img{position:relative}
.mb-editor-popup .mb-popup-dots{display:flex;justify-content:center;gap:5px;margin:-18px 0 6px;position:relative;z-index:1}
.mb-editor-popup .mb-popup-dot{width:6px;height:6px;border-radius:50%;background:rgba(255,255,255,.55);box-shadow:0 0 2px rgba(0,0,0,.35)}
.mb-editor-popup .mb-popup-dot.on{background:#fff}
.mb-editor-popup .mb-popup-count{font-size:11px;color:var(--dim);margin:0 0 6px}
.mb-editor-popup .mb-popup-title{font-weight:600;margin:0 0 4px}
.mb-editor-popup .mb-popup-desc{margin:0;white-space:pre-line}
.mb-editor-popup .mb-popup-actions{display:flex;flex-direction:column;gap:6px;margin-top:8px}
.mb-editor-popup .mb-popup-btn{box-sizing:border-box;display:flex;align-items:center;justify-content:center;gap:6px;width:100%;
  padding:7px 10px;border-radius:6px;font:600 12.5px/1.2 inherit;cursor:default}
.mb-editor-popup .mb-popup-btn svg{width:14px;height:14px;flex:none}
.mb-editor-popup .mb-popup-btn-nav{background:#2d7ff9;color:#fff}
.mb-editor-popup .mb-popup-btn-link{background:#eef1f4;color:#1e2328}

/* Appearance/Popup tabs splitting the per-shape inspector — reuses the real
   .codetabs/.codetab component the Publish dialog's Links&Embeds/Analytics
   tabs already use, rather than a new tab style. */
.tabpanel{display:none}
.tabpanel.sel{display:block}

/* ======================================================================
   Mobile shell (design/mobile-option-d-hamburger-sheet.html, approved
   2026-07-29 — see planning/decisions.md). The desktop shell above is four
   position:fixed panes at fixed pixel widths with nowhere to shrink to on
   a phone; below the breakpoint the rail disappears behind a hamburger
   (js/mobile.js), and .left/.right become bottom sheets instead of side
   columns; the rail's tabs become a bottom tab bar (#tabbar, 2026-09-26).
   New elements (hamburger/mobilemenu/sheethandle/tabbar) are inert
   above the breakpoint so they never affect desktop layout. */
.hamburger,.mobilemenu,.sheethandle,.mobile-back,.mobile-close,.mobile-settings,.addmenu-close,.addmenu-head,.tabbar,#add-menu-import{display:none}

.fab-edit,.draw-hint,.fab-done{display:none}

.hamburger{width:36px;height:36px;border:none;background:none;border-radius:8px;color:var(--ink);cursor:pointer;flex:none}
.hamburger:hover,.hamburger.active{background:var(--hover)}
.hamburger svg{width:19px;height:19px;stroke:currentColor;stroke-width:1.8;fill:none;stroke-linecap:round;stroke-linejoin:round}

/* full-screen menu page the hamburger opens — the top-bar actions that
   don't fit a phone-width bar (Save/Publish/Download/Help); the rail's
   tabs are in #tabbar instead */
.mobilemenu{position:fixed;inset:0;background:var(--panel);z-index:150;flex-direction:column;transform:translateX(-100%);transition:transform .24s ease}
body.mobile-menu-open .mobilemenu{transform:translateX(0)}
.mm-head{display:flex;align-items:center;gap:6px;height:var(--topH);padding:0 8px 0 4px;border-bottom:1px solid var(--line);flex:none}
.mm-head .ttl{font-weight:680;font-size:15px;flex:1}
.mm-body{flex:1;overflow-y:auto}
.menurow{display:flex;align-items:center;gap:12px;height:54px;padding:0 16px;border:none;border-bottom:1px solid var(--line);color:var(--ink);background:none;width:100%;text-align:left;font:600 14px var(--font);cursor:pointer;text-decoration:none}
/* black on gray, not the mode's blue/green accent (Aaron, 2026-09-26) */
.menurow .mic{width:30px;height:30px;border-radius:8px;background:var(--hover);display:grid;place-items:center;color:var(--ink);flex:none}
.menurow .chev{margin-left:auto;color:var(--faint);display:flex}
.menudivider{height:10px;background:var(--hover)}
.menurow:disabled{opacity:.4;cursor:default}
.mm-dot,.hamburger .hb-dot{width:8px;height:8px;border-radius:50%;background:#e0433b;flex:none}
.mm-dot{margin-left:2px}
.mm-dot[hidden],.hamburger .hb-dot[hidden]{display:none}
.hamburger{position:relative}
.hamburger .hb-dot{position:absolute;top:6px;right:5px;border:1.5px solid var(--panel);box-sizing:content-box}
/* Map/Print switch at the top of the menu (mobile's stand-in for #modetoggle) */
.mm-modes{display:flex;background:var(--hover);border-radius:999px;padding:3px;gap:2px;margin:12px 16px}
.mm-modes button{flex:1;border:none;background:none;padding:9px 0;border-radius:999px;font:650 13px/1 var(--font);color:var(--dim);cursor:pointer}
.mm-modes button.on{background:var(--panel);color:var(--ink);box-shadow:0 1px 3px rgba(20,30,45,.18)}
/* each row belongs to one mode; body.print-mode (js/printmode.js) picks the set */
body.print-mode .mobilemenu [data-mmode="map"],
body:not(.print-mode) .mobilemenu [data-mmode="print"]{display:none}
/* Map-edit detour (Print's "Edit map"): same exclusions as js/printmode.js's
   applyNav() — no Share or Download from inside it (the tab bar's My maps
   follows applyRail() through its .railbtn, js/mobile.js) */
body.map-edit .mobilemenu #mm-download{display:none}

/* Bottom tab bar (mobile only, js/mobile.js): two tabs, "+", two tabs. Fixed
   columns, so a tab that's hidden (My maps in the Edit-map detour) leaves a
   gap rather than pulling "+" off center. */
.tabbar{position:fixed;left:0;right:0;bottom:0;height:var(--tabH);padding-bottom:env(safe-area-inset-bottom,0px);background:var(--panel);
  border-top:1px solid var(--line);z-index:145;grid-template-columns:repeat(5,1fr);align-items:center}
.tabbar .tab{grid-row:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;height:100%;min-width:0;
  border:none;background:none;color:var(--faint);font:600 11px/1 var(--font);cursor:pointer;padding:0}
.tabbar .tab[hidden]{display:none}
.tabbar .tab .lbl{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}
.tabbar .tab .ico{width:22px;height:22px}
body.mobile-panel-open .tabbar .tab.on{color:var(--ink)}
.tabbar [data-panel="preset"],.tabbar [data-panel="template"]{grid-column:1}
.tabbar [data-panel="layers"],.tabbar [data-panel="printsettings"]{grid-column:2}

.tabbar [data-panel="overlay"],.tabbar [data-panel="design"]{grid-column:4}
.tabbar [data-panel="mymaps"],.tabbar [data-panel="saveddesign"]{grid-column:5}
.tabbar .tabplus{grid-row:1;grid-column:3;justify-self:center;display:grid;place-items:center;width:46px;height:46px;border-radius:50%;border:none;
  background:var(--accent);color:#fff;box-shadow:0 4px 12px rgba(20,30,45,.22);cursor:pointer}
.tabbar .tabplus:active{filter:brightness(.94)}
.tabbar .tabplus .ico{width:22px;height:22px}

/* shared drag-handle + close bar dropped into .left/.right for mobile —
   one instance each, not per-panel, since only one panelbody shows at a
   time (js/ui.js switchPanel) and .right's content is a single template */
.sheethandle{align-items:center;justify-content:center;position:relative;height:26px;flex:none;cursor:ns-resize;touch-action:none}
.sheethandle .grip{width:36px;height:4px;background:var(--line-2);border-radius:99px}
.sheethandle .navicon{position:absolute;right:6px;top:1px}

@media (max-width: 760px) {
  /* left-aligned, not centered — its box already starts flush with the
     search bar's edge below it (both left:12px), so centering the icon
     inside a wider tap target visually offset it from that edge */
  .hamburger{display:flex;align-items:center;justify-content:flex-start}
  .mobile-close,.mobile-settings,.addmenu-close{display:grid;place-items:center}
  /* No Back button on any bottom sheet (Aaron, 2026-09-25): .mobile-back stays
     display:none from the base rule; Close (X) is the one way out. !important:
     `.dropmenu button{display:flex}` would otherwise show the add-menu's. */
  .mobile-back{display:none!important}
  /* the tab bar's "+" (#tab-plus) does this job now — hide the
     desktop version, replaced by #overlay-settings-btn (Overview panel
     settings) in the same header row */
  #overlay-add{display:none}
  /* mobile keeps the #overlay-add-menu full-sheet flow (every type,
     Section included) via #tab-plus instead of this canvas-docked
     toolbar — no room for a floating bar on a small screen, and it would
     sit under the bottom sheet/hamburger chrome anyway */
  #overlaybar{display:none}
  .mobilemenu{display:flex}
  .sheethandle{display:flex}
  .tabbar{display:grid}
  /* The tab bar's height: the canvas ends above it, and the Edit bar and
     toasts sit above it while no sheet is up. 0 while the export flow or
     crop focus own the screen (the bar hides then, and the canvas reclaims
     the space). Selecting an element or drawing a line do NOT zero this —
     seebelow, they slide the bar away visually instead without resizing
     the canvas underneath it (Aaron, 2026-09-28: "the canvas moves a bit,
     we should not move it" — .canvas's own box used to shrink to 0 top/tab
     insets on selection, and since the poster centers inside it, growing
     the box by different amounts top vs. bottom recentered the poster by a
     few px, a visible jump with no JS re-fit involved at all). */
  body{--tabH:calc(58px + env(safe-area-inset-bottom,0px))}
  body.export-mode,body.print-focus{--tabH:0px}
  /* WHEN each piece of chrome shows (tab bar, top bar, action bar, search)
     is decided in ONE place, js/uistate.js derive(), and written onto
     <body> as data-tabbar / data-topbar / data-actionbar / data-search;
     the rules below only say what each answer looks like (build-step
     migration Phase 5, 2026-09-29; dev/uistate-test.js holds every flag
     combination to the same result as the selector chains it replaced). */
  body[data-tabbar="gone"] .tabbar{display:none}
  /* .top itself is fully hidden below (unlike export-mode/print-focus, which
     only hide its BUTTONS and keep the bar's own height reserved) — so
     .canvas/.rail/.left/.right's shared `top:var(--topH)` needs zeroing
     here too, or the canvas would leave a blank strip where the bar was.
     Export/print-focus are the only two that still resize the canvas this
     way — they're genuine full-screen takeovers, not "an element got
     selected," so the same jump risk doesn't apply the same way there and
     nobody has reported one. */
  /* Any bottom sheet up (a tab's, the properties page, either "+" sheet)
     hides the tab bar, and closing it brings the bar back (Aaron,
     2026-09-26). So sheets sit on the screen's bottom edge, never above
     the bar. Selecting an element (Aaron, 2026-09-28: "hide the bottom tab
     bar and the top nav bar... only show lock/del/edit") and drawing a
     line (Aaron, 2026-09-28: "hide the top bar" — the tab bar already did)
     join this same slide-away treatment, animated (Aaron, 2026-09-28: "add
     subtle animation") rather than the export/print-focus pair's instant
     display:none, and WITHOUT touching --tabH — the canvas stays put, body's
     own background (same colour as .canvas) shows through underneath while
     the bar is slid away, so it still reads as "the canvas got more room"
     with zero layout change. (data-tabbar="away"; "gone" is the
     export/print-focus display:none above.) */
  .tabbar{transition:transform .22s ease}
  body[data-tabbar="away"] .tabbar{transform:translateY(100%);pointer-events:none}
  /* where a closed sheet slides to: off the bottom, shadow and all */
  body{--sheet-away:calc(101% + 40px)}
  /* No expand buttons on phones: a sheet grows by dragging its header */
  #preset-expand,#mymaps-expand,#tpl-expand,#saveddesign-expand{display:none}
  /* Print's Map panel: "choose a map" (load a different saved map) and
     "Edit map" (the full-screen map-editing detour) are both desktop-only
     for now (Aaron, 2026-09-27) — Adjust view (panning/framing the shot)
     stays, so it goes full-width on its own instead of sharing .actrow. */
  .mapstylepreview,#pd-editmap{display:none}
  #pd-pan{grid-column:1/-1}
  /* "Clip content" is on by default everywhere now (js/printdesign.js) — on
     mobile there's no way to turn it back off, so the toggle itself is
     pointless chrome sitting over the stage (Aaron, 2026-09-27). */
  .posterclip{display:none}
  /* The whole header row drags the sheet, not just the grip (js/mobile.js
     wireSheetDrag); its buttons still just click */
  .lhead,.insp-head,.addmenu-head{touch-action:none}

  /* ---- top bar: hamburger replaces the rail as the mode switcher;
     Save/Help/Publish/Download move into the mobile menu (below) so the
     bar itself only holds what's worth one tap at all times. Left padding
     added to match the existing 12px on the right (the hamburger used to
     sit flush at the very edge with none). ---- */
  .top{padding-left:12px}
  /* redo sits right up against the account/login button once Save/Help/
     Publish/Download and their .vsep separators are hidden below — there's
     nothing left between them but .top's 4px flex gap */
  /* desktop's padding:0 12px was sized for a big branding cell — on top of
     .top's own gap:4px after the hamburger, that read as too much space */
  .logo{width:auto;padding-left:0}
  .doctitle{display:none} /* rename/duplicate/delete still reachable via the My maps panel's own per-card icons */
  #doctitle-drop{display:none}
  /* A saved design open in Print: its name (with the design menu's chevron)
     takes the logo's place (Aaron, 2026-09-30: "when users open a saved
     design, right now we dont show the name ... hide the logo, and show the
     name with the chevron icon"). The name shrinks with an ellipsis so
     Purchase always fits. Tapping it renames inline, at 16px so iOS
     doesn't zoom the page. */
  body.print-mode.design-open .logo{display:none}
  body.print-mode.design-open #designtitle:not([hidden]){display:flex;flex:0 1 auto;min-width:0;max-width:none;padding:0 4px 0 6px}
  body.print-mode.design-open #designtitle .dt-name{font-size:15px}
  body.print-mode.design-open #designtitle-drop{flex:none}
  body.print-mode.design-open .dt-input{font-size:16px;width:min(220px, 50vw)}
  #btn-save,#drop-help-wrap,#btn-export,.top>.vsep{display:none}
  /* iOS zooms the whole page when a focused field's text is under 16px and
     doesn't always zoom back out, which left the fixed top bar scrolled out of
     view after editing in a sheet (Aaron, 2026-09-25). Sheet fields stay 16px. */
  .left input,.left select,.left textarea,.right input,.right select,.right textarea,.proptext,.dlg-input{font-size:16px}
  /* Map/Print switch lives in the hamburger menu (#mm-modes) here */
  .modetoggle{display:none}
  /* Undo/Redo/Notifications live in the hamburger menu here (#mm-undo etc.).
     The notif dropdown's own anchor (its hidden icon) has no place on the
     bar, so the panel itself pins under the top bar instead. */
  #btn-undo,#btn-redo,#btn-notif,#notif-dot{display:none}
  /* Top right: Map mode shows Share (#btn-publish), Print shows only
     Purchase. Login moves into the menu (#mm-account); the account dropdown
     it opens pins under the bar like the notification panel. */
  /* Only the button hides, not its wrapper: #menu-account is a child of it and
     a display:none parent would never render the sheet. js/mobile.js also moves
     #menu-account out of .top (z-index 40) so it can sit above the sheets. */
  #btn-account,#btn-share-design{display:none}
  #drop-account-wrap{margin:0}
  /* Account menu = a bottom sheet (opened from the hamburger menu's account row) */
  #menu-account{position:fixed;top:auto;left:0;right:0;bottom:0;width:auto;max-height:75vh;border:none;border-radius:16px 16px 0 0;
    box-shadow:0 -10px 30px rgba(20,30,45,.2);padding:0 0 calc(8px + env(safe-area-inset-bottom,0px));z-index:210;background:var(--panel)}
  .acct-sheet-head{display:flex;align-items:center;justify-content:space-between;height:44px;padding:0 8px 0 16px;border-bottom:1px solid var(--line);
    font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--dim);position:sticky;top:0;background:var(--panel)}
  .acct-sheet-head button{width:36px;height:36px;border:none;background:none;font-size:22px;line-height:1;color:var(--dim);cursor:pointer}
  #menu-account .acct-info{padding:12px 16px;margin:0}
  #menu-account .acct-email{font-size:15px}
  #menu-account .acct-tier{font-size:13px}
  #menu-account .acct-cta{width:calc(100% - 32px);margin:12px 16px 4px}
  #menu-account .acct-hint{padding:0 16px 8px}
  #menu-account .acct-item{padding:14px 16px;font-size:16px;border-radius:0;border-bottom:1px solid var(--line)}
  #menu-account .acct-legal{padding:12px 16px 4px;font-size:13px}
  /* admin Activity feed opens from that menu: same bottom-sheet treatment */
  #menu-freeexp{position:fixed;top:auto;left:0;right:0;bottom:0;width:auto;max-height:75vh;border:none;border-radius:16px 16px 0 0;box-shadow:0 -10px 30px rgba(20,30,45,.2);z-index:210}
  #menu-notif{position:fixed;top:calc(var(--topH) + 4px);left:12px;right:12px;width:auto}
  /* .top has a fixed height everything else's `top:var(--topH)` depends on
     (.rail/.left/.right/.canvas) — scroll this row horizontally instead of
     letting it wrap, which would grow .top and misalign all of those */
  .exportnav{overflow-x:auto}
  .exd-instr{display:none} /* the width/height fields and Continue button are enough without the prose */
  .exd-scale-label{display:none} /* keep the marker/stroke sliders themselves, drop the "Markers"/"Stroke" prose */

  /* ---- icon rail gone; canvas takes the full width the rail+panel used to share ---- */
  .rail{display:none}
  .canvas{left:0;right:0;bottom:var(--tabH)}
  /* no expand/collapse on mobile — js/ui.js's renderTree() always renders
     every category's children there (isMobile() check), so the chevron
     never actually toggled anything left to hide */
  .row .chev{display:none}
  /* same 12px gutter as the hamburger (left) and account button (right)
     above it, not just visually centered — left+right with no width lets
     #search-input's flex:1 fill the space between the icon buttons */
  .maptools{left:12px;right:12px}
  /* #search-input's flex:1 only fills .maptools-panel's own box — but
     .maptools-panel itself had no flex-grow relative to .maptools, so it
     never stretched to use the width .maptools now has, leaving the 3
     tool buttons stranded short of the right edge instead of flush */
  .maptools-panel{flex:1}
  .maptools-panel #search-input{min-width:0}
  /* Print's search bar reaches the right gutter on mobile instead of
     stopping at a fixed 260px, matching the equal 12px inset the icon
     already has on the left (Aaron, 2026-09-27) — .maptools now spans the
     same left/right 12px every other mobile toolbar row does (it used to
     shrink-wrap the collapsed icon's own width); flex:1 fills that real
     width whenever the bar is actually expanded. Collapsed-and-idle needs
     its OWN flex:none here: flex-basis:0 (what flex:1 resolves to) wins
     over any `width` for sizing regardless of which rule has higher
     specificity, so without this the .maptools-panel{flex:1} rule just
     above (every OTHER mobile bar's own stretch-to-fill) kept the 34px
     collapsed box wide too — a real regression this line fixed, not just
     defensive belt-and-suspenders. */
  body.print-mode #searchbox.collapsed{flex:none}
  /* .maptools is the visible white pill (background/border/shadow) — #searchbox
     is just a flex child INSIDE it, so shrinking #searchbox alone (the fix
     above) wasn't enough: .maptools itself was unconditionally right:12px,
     so the pill stayed full-width and empty past the icon even once the
     search box inside it correctly narrowed (Aaron: "the search bar not
     become shorter... untappable except for the search icon" — that dead
     space WAS .maptools's own chrome). :has() makes .maptools's own width
     follow #searchbox's real state instead of being unconditional. */
  body.print-mode .maptools{right:auto}
  /* Selecting anything but the map hides the search bar outright - the
     floating action bar (Lock/Move/Delete/Edit) owns the screen instead;
     map-only selection (or nothing selected) leaves search as-is
     (Aaron, 2026-09-28). Drawing a line hides it too (Aaron, 2026-09-28) -
     nothing to search for mid-drawing, and it only sat there overlapping
     the poster while placing points. (data-search="hidden") */
  body[data-search="hidden"] .maptools{display:none}
  /* The map alone selected (data-search="pinned") slides the top nav bar
     away, leaving its old spot empty; move search up into
     that freed strip instead of leaving it sitting dead (Aaron, 2026-09-28:
     "when the map is selected, we should move the search bar to top").
     Fixed, not absolute: .maptools's usual `top:12px` is relative to
     .canvas, which still starts at the ORIGINAL --topH (the canvas itself
     never resizes on selection, see the --topH/--tabH comment above) - a
     fixed position is what actually reaches the freed strip above it. */
  body[data-search="pinned"] .maptools{position:fixed;top:8px;left:12px;right:12px}
  body.print-mode .maptools:has(#searchbox:not(.collapsed)),
  body.print-mode .maptools:has(#searchbox.collapsed:hover),
  body.print-mode .maptools:has(#searchbox.collapsed:focus-within),
  body.print-mode .maptools:has(#searchbox.collapsed.has-text){right:12px}
  /* .force-collapsed always wins, even if the browser also still insists
     one of the :hover/:focus-within states above matches (the same stuck-
     pointer situation .force-collapsed itself exists to override). */
  body.print-mode .maptools:has(#searchbox.force-collapsed){right:auto!important}
  body.print-mode #searchbox:not(.collapsed),
  body.print-mode #searchbox.collapsed:hover,
  body.print-mode #searchbox.collapsed:focus-within,
  body.print-mode #searchbox.collapsed.has-text{flex:1;width:auto}

  /* ---- .left/.right: side columns become ONE bottom sheet on mobile, not
     two. .left is the sheet itself (slides up/down); .right is a second
     "page" of that same sheet — same position/height, sliding over it
     horizontally instead of opening its own sheet from below — so picking
     a feature/shape reads as paging forward within one sheet, and its
     back button pages back to whatever list .left is already showing
     underneath, rather than closing anything. js/ui.js's
     setInspectorVisible() adds mobile-panel-open whenever the inspector
     becomes visible, so .left (the base the page slides over) is always
     open first. ---- */
  /* --sheet-h is the one source of truth for both pages' height — set by
     class below (tap-toggle) or live by js/mobile.js's drag handler
     (inline style on body always wins over these class rules) */
  body{--sheet-h:52vh}
  body.sheet-expanded{--sheet-h:80vh}
  body.sheet-peek{--sheet-h:72px} /* grip + title only; KEEP IN SYNC with PEEK_PX in js/mobile.js */
  /* dragging sets height every pointermove frame — the .22s transition
     would fight/lag behind the finger, so it's suspended while active */
  body.sheet-dragging .left,body.sheet-dragging .right{transition:transform .22s ease}

  .left{position:fixed;top:auto;left:0;right:0;bottom:0;width:auto;height:var(--sheet-h);border-right:none;border-radius:16px 16px 0 0;box-shadow:0 -10px 30px rgba(20,30,45,.2);z-index:120;display:flex;flex-direction:column;transform:translateY(var(--sheet-away));transition:transform .22s ease,height .22s ease}
  body.mobile-panel-open .left{transform:translateY(0)}
  .panelbody{height:auto;flex:1;min-height:0}
  /* #overlay-add-menu is nested inside .left (needed for its desktop
     anchored-dropdown position), and .left's own transform (above) makes
     it BOTH the containing block AND a new stacking context for that
     fixed-position descendant — so #overlay-add-menu's z-index:135 only
     ever gets compared against .left's OWN z-index (120) at the top
     level, never directly against .right's (125.) .right wins that
     comparison and paints over the entire .left subtree, add-menu
     included, no matter how high a z-index the add-menu itself claims.
     Bumping .left's own z-index above .right's while the add-menu is
     specifically open (js/mobile.js watches #overlay-add-menu's hidden
     attribute) is the only fix that doesn't restructure the DOM and
     break desktop's dropdown anchoring. */
  body.mobile-addmenu-open .left{z-index:140}
  /* The "+" sheet fits its content, so it's shorter than .left, which it
     lives inside; .left's own grip and list showed above it as a second
     sheet. visibility (not display) so the nested add menu can opt back in. */
  body.mobile-addmenu-open .left{visibility:hidden}
  body.mobile-addmenu-open #overlay-add-menu{visibility:visible}

  .right{position:fixed;top:auto;bottom:0;left:0;right:0;width:auto;height:var(--sheet-h);border-left:none;border-radius:16px 16px 0 0;box-shadow:-10px 0 30px rgba(20,30,45,.2);z-index:125;transform:translateX(100%);transition:transform .22s ease,height .22s ease}
  .right:not([hidden]){transform:translateX(0)}

  /* Print's add-element toolbar (#designbar) becomes a bottom sheet: eight
     tools in a row ran off the right edge of a phone. Same element and
     buttons, so js/printdesign.js's handlers are untouched; the tab bar's
     #tab-plus (js/mobile.js) toggles body.mobile-tools-open. */
  .designbar{position:fixed;left:0;right:0;bottom:0;top:auto;transform:translateY(101%);visibility:hidden;
    display:grid;grid-template-columns:repeat(3,1fr);gap:0;border:none;border-radius:16px 16px 0 0;
    padding:0 0 calc(16px + env(safe-area-inset-bottom,0px));max-height:85vh;overflow-y:auto;
    box-shadow:0 -10px 30px rgba(20,30,45,.2);z-index:135;transition:transform .22s ease,visibility 0s .22s}
  .designbar[hidden]{display:none}
  body.mobile-tools-open .designbar{transform:translateY(0);visibility:visible;transition:transform .22s ease}
  /* #designbar lives inside .canvas (z-index:10, its own stacking context), so
     it can never paint above .left/.right (120/125). One sheet at a time
     instead: the tab sheet slides away while this one is up. */
  body.mobile-tools-open .left,body.mobile-tools-open .right{transform:translateY(var(--sheet-away))}
  .designbar .sheethandle,.designbar .addmenu-head{grid-column:1/-1}
  /* its header is the same grip + 38px title row as every other sheet's */
  .designbar .addmenu-head{margin-bottom:6px}
  .designbar .addmenu-head .navicon{flex-direction:row;width:32px;height:32px}
  .designbar .obsep{grid-column:1/-1;width:auto;height:1px;margin:6px 12px}
  .designbar button[data-tool],.designbar button[data-add]{width:auto;height:72px;font-size:12px;gap:6px;margin:2px 6px}
  /* Move/Pan are for a mouse; on a phone one finger moves, pinch pans/zooms */
  .designbar button[data-tool],.designbar .obsep-tools{display:none}
  /* a finger dragging a "+" sheet's header moves it live (js/mobile.js) */
  body.sheet-dragging .designbar,body.sheet-dragging #overlay-add-menu{transition:none}
  /* one-finger drags on the stage are handled in JS (wireMobileTouch) */
  body.print-mode #posterstage{touch-action:none}
  /* The stage reaches UNDER the top nav bar and the tab bar, padded back by
     the same amounts so the poster's fitted size and centre are unchanged.
     The bars slide away on selection/drawing without .canvas resizing (see
     the .tabbar comment above), and a stage clipped to .canvas left those
     two strips as dead grey bands cutting off a zoomed poster (Aaron,
     2026-09-29: "the top bar disappear, but there is still a gray bar there
     covering canvas", back after part 12 only fixed the map-selected case).
     While the bars are up they sit above .canvas and cover the overhang.
     js/printdesign.js stageOverhang() reads these offsets back so the fit
     math and its inline padding-bottom account for them. Both vars are 0
     in export-mode/print-focus, so the overhang disappears there too. */
  body.print-mode .posterstage{top:calc(-1 * var(--topH));bottom:calc(-1 * var(--tabH));padding-top:calc(var(--poster-pad,40px) + var(--topH))}
  /* Edit: appears after a tap on an element, not after a drag. Above the tab
     sheet like the "+" is, and gone whenever another surface owns the screen
     (data-actionbar: "above-sheet" here, "bottom" below, "hidden") */
  body[data-actionbar="above-sheet"] .fab-edit,
  body[data-actionbar="bottom"] .fab-edit{display:flex;align-items:center;gap:10px;position:fixed;left:50%;transform:translateX(-50%);
    bottom:calc(var(--sheet-h) + 16px);z-index:150;transition:bottom .22s ease}
  /* The top nav bar goes too, same "one thing owns the screen" reasoning as
     the tab bar just above. Slides away like the tab bar (Aaron,
     2026-09-28: animation, and no --topH change so .canvas never
     resizes/moves the poster — see the .tabbar comment above). It STAYS
     away once Edit opens an element's properties sheet (Aaron, 2026-09-29:
     "when users tap edit, the top bar shows up. we should keep it hidden").
     (data-topbar="hidden") */
  .top{transition:transform .22s ease}
  body[data-topbar="hidden"] .top{transform:translateY(-100%);pointer-events:none}
  .fab-edit button{display:flex;align-items:center;justify-content:center;gap:6px;height:44px;border:none;border-radius:999px;cursor:pointer;font:650 14px var(--font)}
  .fab-edit .edit{padding:0 20px;background:var(--accent);color:#fff;box-shadow:0 8px 20px rgba(20,184,152,.4)}
  .fab-edit .del{width:44px;background:var(--panel);color:#d33a31;border:1px solid var(--line);box-shadow:0 8px 20px rgba(20,30,45,.18)}
  /* nothing to delete when only the map is selected (it can't be removed) */
  body.mobile-edit-nodel .fab-edit .del:not(.lock):not(.move){display:none}
  .fab-edit .lock{color:var(--dim)}
  .fab-edit .lock.on{color:var(--accent);border-color:var(--accent-line)}
  /* Move map only makes sense for the map itself — same condition Delete's
     own rule above uses, just the opposite way round. */
  .fab-edit .move{color:var(--dim)}
  body:not(.mobile-edit-nodel) .fab-edit .move{display:none}
  /* locked (js/printdesign.js): no handles, so nothing to grab */
  .pel.locked .resizehandle,.pel.locked .sidehandle,.pel.locked .rothandle,.pel.locked .pel-node,
  .postergroup.locked .resizehandle,.postergroup.locked .sidehandle{display:none!important}
  /* Hardcoded rather than var(--tabH)-based: --tabH no longer zeroes for a
     selection (the tab bar slides away without resizing .canvas, above),
     so computing off it here would float Edit 58px too high, floating
     over a "gap" that's really just the tab bar hidden underneath it. */
  body[data-actionbar="bottom"] .fab-edit{bottom:calc(16px + env(safe-area-inset-bottom,0px))}

  /* Overlay lhead's own "+" (#overlay-add-menu): a full paging sheet here
     instead of a small anchored dropdown — same [data-add] buttons/click
     handlers js/overlay.js already wires, just restyled/repositioned.
     Sits above #inspector (z-index 125) so it takes over cleanly even
     while a shape's properties or Overview settings happen to be paged
     over .left already. */
  #overlay-add-menu{position:fixed;top:auto;bottom:0;left:0;right:0;width:auto;min-width:0;height:auto;max-height:var(--sheet-h);background:var(--panel);border:none;border-radius:16px 16px 0 0;box-shadow:-10px 0 30px rgba(20,30,45,.2);padding:0;z-index:135;display:flex;flex-direction:column;transform:translateX(100%);transition:transform .22s ease,height .22s ease}
  #overlay-add-menu[hidden]{display:none} /* display:flex above would beat the hidden attr */
  #overlay-add-menu:not([hidden]){transform:translateX(0)}
  .addmenu-head{display:flex;align-items:center;justify-content:space-between;height:38px;padding:0 12px;border-bottom:1px solid var(--line);flex:none}
  /* .dropmenu button{width:100%;padding:7px 10px} (desktop, unscoped)
     otherwise wins over .navicon's own width/padding — element+class
     beats a lone class — since these buttons are still .dropmenu
     descendants. display/place-items deliberately left unset here so
     .mobile-back's left-alignment below still applies (that rule would
     lose to a display set on this more specific selector). */
  .addmenu-head .navicon{width:32px;padding:0;flex:none}
  .addmenu-head .title{font-size:13px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--dim)}
  .addmenu-body{flex:1;overflow-y:auto}
  /* Same tile grid as Print's "+" sheet (#designbar above), Aaron 2026-09-26 */
  #overlay-add-menu .addmenu-body{display:grid;grid-template-columns:repeat(3,1fr);align-content:start;padding:6px 0 calc(16px + env(safe-area-inset-bottom,0px))}
  #overlay-add-menu button[data-add],#overlay-add-menu #add-menu-import{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;
    width:auto;height:72px;margin:2px 6px;padding:0 4px;border:none;border-radius:8px;background:none;color:var(--dim);font-size:12px;line-height:1.1;text-align:center}
  #overlay-add-menu button[data-add]:active,#overlay-add-menu #add-menu-import:active{background:var(--hover);color:var(--ink)}

  /* back-chevron + title on the left, Close on the right — the whole
     header box matches .lhead's exactly (height/padding), the crumb is
     dropped (a single line, like .lhead's), and the title reuses
     .lhead .ttl's look — the two "pages" of one sheet share one real
     header, not two differently-sized, differently-styled ones */
  .insp-head{display:flex;align-items:center;justify-content:space-between;height:38px;padding:0 12px}
  .insp-head-left{display:flex;align-items:center;gap:8px;min-width:0}
  .insp-head .crumb{display:none}
  .insp-head .title{font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--dim)}

  /* .insp-head reuses .lhead-actions to group a panel's own action button
     (if any) with the new Close, so .lhead's justify-content:space-between
     still only ever sees two items (title, this group) — the class's own
     display:flex (base rule, above) already applies here too. */

  /* the full-screen menu suppresses both — .right needs .left open
     beneath it, so opening the menu (which closes .left) has to slide
     .right fully away too, not strand it mid-page over a closed sheet */
  body.mobile-menu-open .left{transform:translateY(var(--sheet-away))}
  body.mobile-menu-open .right{transform:translateX(101%)}

  /* ---- Download dialog: the order-summary rail was designed for 720px;
     stack it instead of squeezing .split-controls into a sliver ---- */
  .dialog.split{height:min(700px,88vh)}
  .split-body{flex-direction:column;overflow-y:auto}
  .split-summary{width:auto;border-left:none;border-top:1px solid var(--line)}

  /* .supportbanner only had max-width, never a real width — an
     absolutely-positioned flex row with width:auto sizes to its
     min-content in Chromium (one word per line) instead of filling the
     available space, only visible now that anything's tested this narrow */
  .supportbanner{width:min(90vw,620px)}

  /* ---- everything below was sized for a mouse at desktop distance —
     bump the base size plus every explicit font-size/icon-size that
     doesn't inherit it, so nothing on the mobile shell stays tiny ---- */
  body{font-size:15px}
  /* one size for every icon on mobile, regardless of data-icon-size —
     .sm/.md existed to fit desktop's tighter chrome, and left redo/undo +
     the search bar's icons (no size attribute, plain .ico) visibly bigger
     than everything using .sm (menurow .mic, ovrow, back/close, etc.) */
  .ico,.ico.sm,.ico.md{width:18px;height:18px}

  .row{height:36px}
  .ovrow{font-size:15px;padding:9px 8px}
  .menurow{font-size:16px}
  .lhead .ttl,.insp-head .title,.sec-top .st{font-size:13px}
  .presetcard .pname{font-size:14px}
  .fieldlabel{font-size:13px}
  .hint{font-size:13px}
  .dropzone{font-size:14px}
  .plabel{width:56px;font-size:14px}
  .colorfield .hex{font-size:14px}
  .rangefield .rv{font-size:13px;width:44px}
}

/* ---- Share map (link-based edit access, 2026-08-01) ---------------------- */
.sharedlg{width:440px}
.sharebody{padding:16px}
.sharesub{margin:0 0 14px;font-size:12.5px;line-height:1.5;color:var(--dim)}

/* Share-a-design dialog (js/printshare.js). The picture leads: it is both
   what the recipient is being offered and, via Copy image, the thing most
   people actually paste into a chat. Capped in height so a tall portrait
   poster can't push the link and the Done button off a short window. */
/* Share a design (js/printshare.js, design/sharedesign-option-a-sidebyside.html).
   Its own class names throughout rather than sharing .sharebody/.sharesub/
   .share-linkcard/.sharefoot with the collaborate dialog — see map.html's
   comment there: a second element wearing those classes silently steals every
   unscoped document.querySelector for them.

   Landscape, unlike every other dialog in the app. A 4:5 poster centred in a
   430px box has to be small and leaves two dead gutters; its own column lets
   it be big and puts the actions in the width it was wasting. */
/* Taller than the app's other dialogs, and the only one whose height depends
   on the user's own artwork — so head and foot stay pinned and the middle
   scrolls rather than the whole thing running off a short window. */
.dsharedlg{width:600px;max-height:88vh;display:flex;flex-direction:column}
.dsharedlg .dlg-head,.dsharedlg .dlg-foot{flex:none}
.dshare-split{overflow-y:auto}
.dshare-split{display:grid;grid-template-columns:auto 1fr;gap:20px;padding:18px;align-items:start}
/* No thumbnail (the capture failed): collapse to one column rather than
   leaving a hole where the poster should be. */
.dshare-split.no-thumb{grid-template-columns:1fr}
.dshare-split.no-thumb .dshare-postercol{display:none}

.dshare-postercol{display:flex;flex-direction:column;gap:9px;align-items:center}
.dshare-postercol img{max-width:210px;max-height:250px;border-radius:9px;display:block;
  background:var(--hover);border:1px solid var(--line)}
.dshare-postercol img:not([src]){display:none}
.dshare-name{font-size:11.5px;font-weight:650;color:var(--dim);text-align:center;max-width:210px;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

.dshare-actions{display:flex;flex-direction:column;min-width:0}
.dshare-promise{font-size:13px;line-height:1.55;color:var(--dim);margin:0 0 14px}
.dshare-promise b{color:var(--ink);font-weight:650}
.dshare-label{font-size:10.5px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--faint);margin-bottom:6px}
.dshare-linkrow{display:flex;gap:8px;margin-bottom:6px}
.dshare-linkrow[hidden]{display:none}
.dshare-linkfield{flex:1;min-width:0;height:34px;border:1px solid var(--line-2);border-radius:7px;
  background:var(--canvas);display:flex;align-items:center;padding:0 10px;font-size:12.5px;font-weight:600;
  color:var(--ink);text-decoration:none;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.dshare-linkfield:hover{color:var(--accent)}
.dshare-copybtn{height:34px;padding:0 14px;border-radius:7px;border:none;background:var(--accent);color:#fff;
  font-size:12.5px;font-weight:650;cursor:pointer;flex:none;display:flex;align-items:center;gap:6px}
.dshare-copybtn:hover{filter:brightness(1.06)}
.dshare-fine{font-size:11.5px;line-height:1.5;color:var(--faint);margin:0 0 16px}

/* "or" between the two ways of handing this over, so Copy the picture reads
   as the alternative to the link rather than a competitor for the same job. */
.dshare-or{display:flex;align-items:center;gap:10px;color:var(--faint);font-size:11px;font-weight:600;
  letter-spacing:.04em;text-transform:uppercase;margin-bottom:12px}
.dshare-or::before,.dshare-or::after{content:"";height:1px;background:var(--line);flex:1}

.dshare-imgbtn{height:34px;border-radius:7px;background:none;border:1px solid var(--accent-line);
  color:var(--accent);font-size:12.5px;font-weight:650;cursor:pointer;display:flex;align-items:center;
  justify-content:center;gap:7px;margin-bottom:7px}
.dshare-imgbtn:hover:not(:disabled){background:var(--accent-soft)}
.dshare-imgbtn:disabled{opacity:.45;cursor:default}

.dshare-status{font-size:12px;line-height:1.5;color:var(--dim);margin:6px 0 0}
.dshare-status:empty{display:none}
.dshare-foot{display:flex;align-items:center;gap:8px}
.dshare-foot .spacer{flex:1}

/* Two columns need the width; below that the poster goes back on top. */
@media (max-width:640px){
  .dsharedlg{width:min(430px,94vw)}
  .dshare-split{grid-template-columns:1fr}
  .dshare-postercol img{max-height:190px}
}
/* the link card is .pub-linkcard, shared with the Publish panel */
#share-linkrow{margin-bottom:10px}
#share-linkrow[hidden]{display:none}
.sharebtns{display:flex;gap:8px}
.sharebtns:empty,.sharebtns:has(> button[hidden]:only-child){display:none}
.shareppl{margin-top:16px;padding-top:14px;border-top:1px solid var(--line)}
/* with nobody added the container is empty, and its divider would draw a
   section rule under nothing */
.shareppl:empty{display:none}
.sharepplhead{font-size:11px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:var(--dim);margin-bottom:8px}
.sharenone{font-size:12px;color:var(--faint)}
/* turn-off sits opposite Done, not beside the link — it's the destructive
   end of the dialog, so it gets the far edge rather than a spot people can
   hit while reaching for Copy */
.sharefoot{align-items:center}
.sharefoot .spacer{flex:1}
/* needs .mini-btn.ghost's specificity to win — and stays neutral rather than
   red: turning the link off is reversible (you can make a new one), so red
   would overstate it next to a real destructive action like Delete */
.mini-btn.ghost.quiet{color:var(--dim);border-color:var(--line-2)}
.mini-btn.ghost.quiet:hover{color:var(--ink);border-color:var(--dim)}
.sharerow{display:flex;align-items:center;gap:8px;padding:5px 0}
.sharerm{width:24px;height:24px;flex:none;color:var(--faint)}
.sharerm:hover{color:#b4341c}
.sharewho{flex:1;min-width:0;font-size:12.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.dlg-foot{display:flex;justify-content:flex-end;padding:12px 16px;border-top:1px solid var(--line)}
/* "Shared" badge on a My-maps card you don't own — sits where the owner's
   rename/delete icons would be, so the row keeps its shape either way */
/* "this map is shared" — on the thumbnail rather than in the name row, so the
   row keeps one consistent shape (name + 3-dot) for every card, and so the
   OWNER sees it on their own map too, not just the borrower. */
.pshared-ic{position:absolute;top:7px;right:7px;width:22px;height:22px;border-radius:50%;
  background:rgba(20,30,45,.58);color:#fff;display:grid;place-items:center;pointer-events:auto}
.pshared-ic svg{width:13px;height:13px;display:block}
/* a menu row that exists but isn't yours to use (Delete, on a shared map) */
.dropmenu button:disabled{color:var(--faint);cursor:default}
.dropmenu button:disabled:hover{background:none}
.dropmenu button:disabled .ico{opacity:.5}

/* ---- presence: who else has this shared map open (2026-08-02) ------------ */
/* One avatar per person, deliberately the same initial-circle as .acct (the
   account button) so "a person" reads the same everywhere in the top bar —
   the green ring is what says "here right now" rather than a different shape.
   The row only exists when someone else IS here; a permanent "just you" chip
   would be noise on every map. */
.presence{display:inline-flex;align-items:center;margin-right:6px}
.presence[hidden]{display:none}
.pres-av{position:relative;width:26px;height:26px;border-radius:50%;
  background:linear-gradient(135deg,#2d7ff9,#7db4ff);color:#fff;
  display:grid;place-items:center;font-size:10px;font-weight:640;cursor:default;
  /* ring drawn OUTSIDE the circle, so it can't shrink the letter's box */
  box-shadow:0 0 0 2px var(--panel),0 0 0 4px #22c55e}
/* overlap when several are here, newest on top — same convention as any
   stacked-avatar row */
.pres-av + .pres-av{margin-left:-6px}
.presence:hover .pres-av{margin-left:2px}
.presence .pres-av:first-child{margin-left:0}

/* Tooltip: our own, not `title` — the native one waits a second and can't be
   styled, and this is a glanceable status, not a description. */
.pres-tip{position:absolute;top:calc(100% + 8px);right:0;white-space:nowrap;
  background:#1e2328;color:#fff;font-size:11.5px;font-weight:500;line-height:1;
  padding:6px 9px;border-radius:6px;opacity:0;pointer-events:none;
  transition:opacity .12s ease;z-index:70}
.pres-av:hover .pres-tip{opacity:1}

/* ======================================================================
   Mobile follow-ups (Aaron, 2026-09-25)
   ====================================================================== */
@media (max-width: 760px) {
  /* 1. The element properties sheet (#inspector) scrolled as a whole, so its
     handle, title and Back/Close went off the top with the content. Only the
     body scrolls now, like every list sheet on .left. */
  .right{overflow-y:hidden}
  /* the style picker was a 174px inner scroller inside the scrolling sheet: it
     clipped the second row of styles into the next section. The sheet scrolls. */
  .stygrid.capped{max-height:none;overflow:visible}
  #insp-body{flex:1;min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch}

  /* 2. Element handles are ~15px, too small for a fingertip. Scaled up, plus an
     invisible ring around each so a near miss still grabs it. The ring is a
     pseudo-element, so the touch target is still the handle itself and
     js/printdesign.js's handlers see the same node. */
  .pel .resizehandle,.pel .sidehandle,.pel .rothandle,
  .postergroup .resizehandle,.postergroup .sidehandle,
  .hproxy .resizehandle,.hproxy .sidehandle,.hproxy .rothandle{transform:scale(var(--hs,1.6))} /* --hs set per element by js/printdesign.js updateHandleScales */
  .pel .resizehandle::after,.pel .sidehandle::after,.pel .rothandle::after,
  .postergroup .resizehandle::after,.postergroup .sidehandle::after,
  .hproxy .resizehandle::after,.hproxy .sidehandle::after,.hproxy .rothandle::after{content:"";position:absolute;inset:-14px}

  .pel .rothandle,.hproxy .rothandle{top:-40px} /* clear of the (now larger) top-edge handle */

  /* Drawing a line: the sheets slide away so the whole poster can be tapped, a
     hint says what to do, and Done finishes (a line with under two points is
     discarded, same as Esc on desktop). */
  body.print-drawing .left,body.print-drawing .right{transform:translateY(var(--sheet-away))}
  body.print-mode.print-drawing .draw-hint{display:block;position:fixed;top:calc(var(--topH) + 64px);left:50%;transform:translateX(-50%);
    z-index:130;max-width:calc(100vw - 32px);padding:8px 14px;border-radius:999px;background:rgba(28,44,60,.88);color:#fff;
    font:600 13px var(--font);white-space:nowrap;pointer-events:none}
  body.print-mode.print-drawing .fab-done{display:flex;align-items:center;justify-content:center;position:fixed;left:50%;transform:translateX(-50%);
    bottom:calc(16px + env(safe-area-inset-bottom,0px));z-index:130;height:44px;padding:0 28px;border:none;border-radius:999px;
    background:var(--accent);color:#fff;font:650 14px var(--font);box-shadow:0 8px 20px rgba(20,184,152,.4);cursor:pointer}

  /* Templates sheet: two cards per row instead of one full-width card each */
  #template-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));grid-auto-rows:min-content;align-content:start;align-items:start;gap:10px}
  #template-list .presetcard{width:auto}

  /* dialogs (z 80) sat under the floating +/Edit buttons (130) and the sheets */
  .dialog-scrim{z-index:200}

  /* Toasts sat at bottom:22px under z 100: behind the tab bar and behind any
     open sheet (Aaron, 2026-09-26: "Sign in to save" never showed). Above
     both now, riding the sheet's top edge like the Edit bar does. #toast
     beats the desktop :has(#overlaybar) nudge, which misfires here because
     the bar is display:none on phones, not [hidden]. */
  #toast{z-index:205;bottom:calc(var(--tabH) + 16px);max-width:calc(100vw - 32px);font-size:14px;text-align:center}
  body.mobile-panel-open #toast{bottom:calc(var(--sheet-h) + 16px)}
  body.mobile-tools-open #toast{top:calc(var(--topH) + 64px);bottom:auto}
  /* Level of detail sits near the right edge of the tool pill; its left-anchored
     dropdown ran 100px off-screen */
  #menu-detail{left:auto;right:0}

  /* 3. Purchase dialog: the whole screen, header and footer fixed, the middle
     scrolls. The footer holds the total and the Continue-to-payment button. */
  #buy-scrim{place-items:stretch}
  #buy-scrim .dialog{width:100vw;max-width:none;height:100vh;height:100dvh;border-radius:0;box-shadow:none;display:flex;flex-direction:column}
  #buy-scrim .dlg-head,#buy-scrim .dlg-foot{flex:none;background:#fff}
  #buy-scrim .dlg-foot{padding-bottom:calc(12px + env(safe-area-inset-bottom,0px))}
  #buy-scrim .buy-body{flex:1;min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch;display:block}
  /* Phones sell the printed poster only, with one full-width Continue and
     "Free shipping" under it; no Digital tab, no Cancel (the x and a tap
     outside still close it), no price beside the button: the size list
     carries it (Aaron, 2026-09-30). Business subscribers keep the Digital
     tab (their plan includes the file): js/printbuy.js render() sets
     .buy-print-only only for everyone else, and .fmt-print on the print tab. */
  #buy-scrim.buy-print-only #buy-format,#buy-cancel{display:none}
  #buy-scrim .dlg-foot{flex-direction:column;align-items:stretch;gap:8px}
  #buy-scrim .dlg-foot-actions{order:1;width:100%}
  #buy-scrim #buy-confirm{flex:1;width:100%;height:46px;font-size:15px}
  #buy-scrim .buy-foot-left{order:2;justify-content:center}
  #buy-total{text-align:center}
  #buy-scrim.fmt-print #buy-total b{display:none}
  #buy-scrim.fmt-print #buy-total .buy-ship{display:block;font-size:12.5px}
  #buy-scrim.fmt-print #buy-total .buy-ship::first-letter{text-transform:uppercase}
}
/* ======================================================================
   Phone Print, step by step (js/printwizard.js; design/printwizard-index.html,
   Aaron 2026-10-02). Everything here is inert until js/printwizard.js puts
   body.print-wizard on, which only happens below the phone breakpoint, in
   Print, without ?ui=classic. body.print-mode.print-steps (a js/uistate.js flag) is the
   five-step sheet; body.print-adv is its Advanced editor (the full editor
   with three tabs). Which bars show is still js/uistate.js derive()'s call:
   print-steps answers tabbar "gone" and search/action bar "hidden". */
.pw-sheet,.pw-shield,.pw-maptools,.pw-draghint,.pw-topback,.pw-advtitle,.pw-topsave,.pw-fullbtn,.pw-movebar,.pw-ihandles{display:none}

@media (max-width: 760px) {
  /* ---- top bar: Save on the right instead of Purchase (Buy is step 5) ---- */
  body.print-wizard #btn-purchase{display:none}
  body.print-wizard .pw-topsave{display:inline-flex;align-items:center;justify-content:center;height:32px;min-width:72px;padding:0 16px;flex:none;border-radius:6px;
    background:none;border:1px solid var(--accent-line);color:var(--accent);font:600 13px/1 var(--font);cursor:pointer}
  /* Advanced editor: a back chevron and the editor's name in place of the
     menu and the logo */
  body.print-adv .hamburger,body.print-adv .logo,body.print-adv #designtitle,body.print-adv #designtitle-drop{display:none!important}
  body.print-adv .pw-topback{display:grid;place-items:center;margin-left:-6px}
  body.print-adv .pw-advtitle{display:block;font:650 15px/1 var(--font);color:var(--ink);white-space:nowrap}

  /* ---- Advanced editor's tab bar: Layers, "+", My designs ---- */
  body.print-adv .tabbar [data-panel="template"],
  body.print-adv .tabbar [data-panel="printsettings"]{display:none}
  body.print-adv .tabbar [data-panel="design"]{grid-column:1}

  /* ---- the steps own the screen ---- */
  /* The canvas ends above the sheet; one fixed height for every step, so
     the poster never jumps between them. */
  body.print-mode.print-steps{--pw-h:min(50vh,470px);--tabH:var(--pw-h);--poster-pad:12px}
  @supports (height:1dvh){body.print-mode.print-steps{--pw-h:min(50dvh,470px)}}
  body.print-mode.print-steps .left,body.print-mode.print-steps .right,body.print-mode.print-steps #designbar{display:none!important}

  /* taps on the poster go nowhere (nothing is selectable in the steps);
     step 2 turns drags and pinches on it into framing the map */
  body.print-mode.print-steps .pw-shield{display:block;position:fixed;left:0;right:0;top:var(--topH);bottom:var(--pw-h);z-index:30;touch-action:none}
  body.print-mode.print-steps[data-pw-step="2"] .pw-shield{cursor:grab}
  body.print-mode.print-steps[data-pw-step="2"] .pw-maptools{display:flex;flex-direction:column;gap:6px;position:fixed;right:12px;bottom:calc(var(--pw-h) + 12px);z-index:31}
  .pw-mt{width:38px;height:38px;border:none;border-radius:10px;background:var(--panel);color:var(--ink);display:grid;place-items:center;box-shadow:0 2px 8px rgba(20,30,45,.18);cursor:pointer}
  body.print-mode.print-steps[data-pw-step="2"] .pw-draghint{display:flex;align-items:center;gap:6px;position:fixed;left:50%;top:calc(var(--topH) + 10px);transform:translateX(-50%);z-index:31;
    background:rgba(30,35,40,.82);color:#fff;font:600 12px/1 var(--font);padding:7px 12px;border-radius:999px;white-space:nowrap;pointer-events:none;transition:opacity .3s}
  .pw-draghint.used{opacity:0}

  /* full screen (lucide expand, top right of the poster's area): the top bar
     and the sheet step aside and the poster takes the whole screen.
     !important: a dragged sheet height sits inline on <body> */
  body.print-mode.print-steps .pw-fullbtn{display:grid;place-items:center;position:fixed;right:12px;top:calc(var(--topH) + 10px);z-index:32;width:38px;height:38px;
    border:none;border-radius:10px;background:var(--panel);color:var(--ink);box-shadow:0 2px 8px rgba(20,30,45,.18);cursor:pointer}
  body.print-mode.print-steps.pw-full{--pw-h:0px!important;--topH:0px!important}
  body.print-mode.print-steps.pw-full .top,body.print-mode.print-steps.pw-full .pw-sheet,
  body.print-mode.print-steps.pw-full .pw-maptools,body.print-mode.print-steps.pw-full .pw-draghint{display:none!important}
  body.print-mode.print-steps.pw-full .pw-fullbtn{top:12px}

  /* step 3: a box with four corner handles round each icon (drag the box to
     move, a corner to resize); the layer sits over the shield, under the
     sheet and the full-screen button */
  body.print-mode.print-steps .pw-ihandles{display:block;position:fixed;inset:0;z-index:31;pointer-events:none}
  .pw-ibox{position:absolute;pointer-events:auto;touch-action:none;cursor:move;outline:1.5px dashed var(--accent);outline-offset:4px;border-radius:2px}
  .pw-ibox.on{outline-style:solid}
  .pw-ih{position:absolute;width:12px;height:12px;border-radius:50%;background:#fff;border:2px solid var(--accent);box-sizing:border-box}
  .pw-ih::before{content:"";position:absolute;inset:-10px} /* a finger-sized target */
  .pw-ih[data-c="nw"]{left:-10px;top:-10px;cursor:nwse-resize}
  .pw-ih[data-c="ne"]{right:-10px;top:-10px;cursor:nesw-resize}
  .pw-ih[data-c="sw"]{left:-10px;bottom:-10px;cursor:nesw-resize}
  .pw-ih[data-c="se"]{right:-10px;bottom:-10px;cursor:nwse-resize}
  /* full screen at step 3, when the poster has an icon: how to use them */
  body.print-mode.print-steps.pw-full.pw-icons[data-pw-step="3"] .pw-movebar{display:flex;align-items:center;gap:8px;position:fixed;left:50%;bottom:calc(16px + env(safe-area-inset-bottom,0px));transform:translateX(-50%);z-index:32;
    background:rgba(30,35,40,.85);color:#fff;font:600 12px/1 var(--font);padding:9px 14px;border-radius:999px;white-space:nowrap;pointer-events:none;transition:opacity .3s}
  body.pw-icondrag .pw-movebar{opacity:0}

  /* the chosen frame round the poster on screen, steps 4 and 5 */
  body.print-mode.print-steps.pw-framed #poster{outline:9px solid var(--pw-fr,#1b1b1b);outline-offset:0}
  body.print-mode.print-steps.pw-framed[data-pw-frame="oak"] #poster{--pw-fr:#b8854a}
  body.print-mode.print-steps.pw-framed[data-pw-frame="black"] #poster{--pw-fr:#1b1b1b}
  body.print-mode.print-steps.pw-framed[data-pw-frame="white"] #poster{--pw-fr:#f1efe9}

  /* ---- the sheet ---- */
  body.print-mode.print-steps .pw-sheet{display:flex;flex-direction:column;position:fixed;left:0;right:0;bottom:0;height:var(--pw-h);z-index:120;background:var(--panel);
    border-radius:16px 16px 0 0;box-shadow:0 -6px 24px rgba(20,30,45,.12)}
  /* the header drags the sheet (js/printwizard.js wireSheetDrag); its
     buttons still just tap */
  .pw-grip,.pw-steps{touch-action:none}
  .pw-grip{flex:none;height:18px;display:flex;align-items:center;justify-content:center;cursor:ns-resize}
  .pw-grip span{width:36px;height:4px;border-radius:99px;background:var(--line-2)}
  body.print-mode.print-steps .pw-sheet{transition:height .22s ease}
  body.print-mode.print-steps.pw-dragging .pw-sheet{transition:none}
  /* dragged to the top: the sheet takes everything below the top bar and
     the poster, which would be too small to be any use, isn't shown. The
     canvas keeps its size underneath (--pw-h untouched), so the poster
     comes back exactly as it was when the sheet is pulled down again. */
  body.print-mode.print-steps.pw-sheet-full .pw-sheet{height:calc(100% - var(--topH))!important;border-radius:0}
  body.print-mode.print-steps.pw-sheet-full .pw-fullbtn,body.print-mode.print-steps.pw-sheet-full .pw-maptools,
  body.print-mode.print-steps.pw-sheet-full .pw-draghint,body.print-mode.print-steps.pw-sheet-full .pw-movebar{display:none!important}
  .pw-steps{display:flex;gap:2px;padding:0 8px 4px;flex:none}
  .pw-steps button{flex:1;min-width:0;display:flex;flex-direction:column;align-items:center;gap:4px;border:none;background:none;padding:2px 0;cursor:pointer;
    font:600 11px/1.1 var(--font);color:var(--faint)}
  .pw-steps .dot{width:22px;height:22px;border-radius:50%;display:grid;place-items:center;border:1.5px solid var(--line-2);font:650 11.5px/1 var(--font);background:var(--panel)}
  .pw-steps .dot svg{width:12px;height:12px}
  .pw-steps .nm{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}
  .pw-steps button.done .dot{background:var(--accent-soft);border-color:transparent;color:var(--accent)}
  .pw-steps button.on{color:var(--ink)}
  .pw-steps button.on .dot{background:var(--accent);border-color:var(--accent);color:#fff}
  .pw-body{flex:1;min-height:0;overflow-y:auto;padding:4px 16px 14px;-webkit-overflow-scrolling:touch}
  .pw-step{display:none}
  .pw-sheet[data-step="1"] .pw-step[data-step="1"],.pw-sheet[data-step="2"] .pw-step[data-step="2"],.pw-sheet[data-step="3"] .pw-step[data-step="3"],
  .pw-sheet[data-step="4"] .pw-step[data-step="4"],.pw-sheet[data-step="5"] .pw-step[data-step="5"]{display:block}
  .pw-actions{flex:none;display:flex;gap:10px;padding:10px 16px calc(10px + env(safe-area-inset-bottom,0px));border-top:1px solid var(--line)}
  .pw-prev,.pw-next{height:46px;border-radius:12px;font:650 15px/1 var(--font);cursor:pointer}
  .pw-prev{flex:none;padding:0 18px;border:1px solid var(--line-2);background:var(--panel);color:var(--ink)}
  .pw-prev[hidden]{display:none}
  .pw-next{flex:1;border:none;background:var(--accent);color:#fff}
  .pw-next.buy{background:#1e8e3e}
  .pw-next:disabled{opacity:.55;cursor:default}
  .pw-lab{display:block;font:700 11.5px/1 var(--font);letter-spacing:.04em;text-transform:uppercase;color:var(--dim);margin:10px 0 8px}

  /* step 1: today's template list, three across */
  .pw-tplslot #template-list{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;padding:2px 0;overflow:visible;flex:none}
  .pw-tplslot #template-list .presetcard{width:auto}
  .pw-tplslot #template-list .presetcard .pname{font-size:12px}
  .pw-tplslot #template-list .pmore{display:none}

  /* step 2 */
  .pw-search{display:flex;align-items:center;gap:8px;height:44px;border-radius:12px;background:var(--hover);padding:0 12px;margin:2px 0 6px;color:var(--dim)}
  .pw-search input{flex:1;min-width:0;border:none;background:transparent;outline:none;font:16px var(--font);color:var(--ink)}
  .pw-search input::-webkit-search-cancel-button{display:none}
  .pw-res{width:100%;display:flex;align-items:center;gap:12px;padding:10px 2px;border:none;border-bottom:1px solid var(--line);background:none;text-align:left;cursor:pointer;color:var(--ink)}
  .pw-res:last-child{border-bottom:none}
  .pw-res .ic{width:32px;height:32px;border-radius:50%;background:var(--hover);display:grid;place-items:center;color:var(--dim);flex:none}
  .pw-res b{display:block;font:600 14px/1.3 var(--font)}
  .pw-res small{display:block;color:var(--dim);font-size:12px;line-height:1.3}
  .pw-resnote{padding:12px 2px;color:var(--dim);font-size:13px}

  /* step 3: a list of the poster's parts, each row opening its own page
     (js/printwizard.js renderList3/renderPage3; design/printstep3, option B) */
  .pw-list{border:1px solid var(--line);border-radius:14px;overflow:hidden;margin-top:4px}
  .pw-li{width:100%;display:flex;align-items:center;gap:12px;min-height:64px;padding:10px 10px 10px 12px;border:none;border-bottom:1px solid var(--line);
    background:var(--panel);color:var(--ink);text-align:left;cursor:pointer}
  .pw-li:last-child{border-bottom:none}
  .pw-li:active{background:var(--hover)}
  .pw-lthumb{width:44px;height:44px;border-radius:10px;overflow:hidden;background:var(--hover);display:grid;place-items:center;color:var(--faint);flex:none}
  .pw-lthumb img,.pw-lthumb svg.pp-map,.pw-lthumb .pp-map{width:100%;height:100%;object-fit:cover;display:block}
  .pw-lthumb > svg:not(.pp-map){width:26px;height:26px}
  .pw-laa{font-size:17px;color:var(--ink)}
  .pw-lt{flex:1;min-width:0}
  .pw-lt b{display:block;font:650 14px/1.25 var(--font)}
  .pw-lt small{display:block;font:500 12.5px/1.35 var(--font);color:var(--dim);margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .pw-lt small.pw-ltext{color:var(--ink);font-size:14px}
  .pw-ldot{display:inline-block;width:10px;height:10px;border-radius:50%;border:1px solid var(--line-2);vertical-align:-1px}
  .pw-chev{color:var(--faint);display:flex;flex:none}
  .pw-p3page.pw-in{animation:pw-slidein .22s ease}
  @keyframes pw-slidein{from{transform:translateX(28px);opacity:0}to{transform:none;opacity:1}}
  /* a page open: it takes the steps bar's place, its header pinned to the
     top of the sheet while the page scrolls */
  body.pw-p3open .pw-steps{display:none}
  body.pw-p3open .pw-body{padding-top:0}
  .pw-ph3{display:flex;align-items:center;gap:6px;height:48px;margin:0 -16px 6px;padding:0 10px;position:sticky;top:0;z-index:2;
    background:var(--panel);border-bottom:1px solid var(--line)}
  .pw-ph3 h3{font:700 16px/1 var(--font);margin:0}
  .pw-back3{width:36px;height:36px;border:none;background:none;border-radius:10px;display:grid;place-items:center;color:var(--ink);cursor:pointer}
  .pw-back3:active{background:var(--hover)}
  .pw-p3body .pw-lab{margin-top:16px}
  .pw-p3body .pw-lab.pw-first{margin-top:2px}
  .pw-themes{display:flex;gap:8px;overflow-x:auto;margin:0 -16px;padding:2px 16px 4px;scrollbar-width:none}
  .pw-themes::-webkit-scrollbar{display:none}
  .pw-themes .stysw{flex:none;width:72px;height:72px}
  .pw-themes .stysw .nm{font-size:9.5px}
  .pw-input{width:100%;height:44px;border:1px solid var(--line-2);border-radius:10px;padding:0 12px;font:16px var(--font);color:var(--ink);outline:none;background:var(--panel)}
  .pw-input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
  /* fonts: four across, each "Aa" in its own face */
  .pw-fontgrid{display:grid;grid-template-columns:repeat(4,1fr);gap:6px}
  .pw-font{height:54px;border-radius:10px;border:1px solid var(--line-2);background:var(--panel);color:var(--ink);cursor:pointer;
    display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;font-size:20px;line-height:1;padding:0}
  .pw-font small{font:500 9.5px/1 var(--font);color:var(--dim)}
  .pw-font.on{border-color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent);background:var(--accent-soft)}
  /* colors: the poster's own, then "+" for any other (a native picker
     stretched invisibly over it) */
  .pw-swatches{display:flex;gap:10px;align-items:center;flex-wrap:wrap;padding:4px}
  .pw-sw{width:32px;height:32px;border-radius:50%;border:1px solid rgba(0,0,0,.14);padding:0;cursor:pointer;position:relative;flex:none}
  .pw-sw.on{box-shadow:0 0 0 2px var(--panel),0 0 0 4px var(--accent)}
  .pw-swplus{background:conic-gradient(#f43,#fc3,#4c6,#3af,#a5f,#f43);border:none;display:block}
  .pw-swplus::after{content:"+";position:absolute;inset:6px;border-radius:50%;background:var(--panel);display:grid;place-items:center;font:700 15px/1 var(--font);color:var(--dim)}
  .pw-swplus input{position:absolute;inset:0;width:100%;height:100%;opacity:0;cursor:pointer;border:0;padding:0}
  .pw-bigphoto{position:relative;width:148px;height:148px;border-radius:14px;overflow:hidden;margin:8px auto 14px;background:var(--hover);display:grid;place-items:center;color:var(--faint)}
  .pw-bigphoto img{width:100%;height:100%;object-fit:cover;display:block}
  .pw-bigphoto.pw-busy{opacity:.5}
  /* the replace button (lucide arrow-left-right), in the middle of the photo */
  .pw-replace{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:44px;height:44px;border-radius:50%;border:none;background:rgba(255,255,255,.94);color:var(--ink);
    display:grid;place-items:center;box-shadow:0 2px 8px rgba(20,30,45,.25);cursor:pointer}
  .pw-movephoto{width:100%;height:40px;font-size:14px}
  .pw-movephoto:disabled{opacity:.45;cursor:default}
  .pw-movehint[hidden]{display:none}
  .pw-p3body .pw-iconpick{display:block}
  /* every icon, straight on the page: it's the page's last item, so the
     page itself scrolls (Aaron, 2026-10-03), no box of its own */
  .pw-p3body .pw-iconpick .iconpick{max-height:none;overflow:visible}
  .pw-hint3{display:flex;align-items:center;gap:7px;margin-top:14px;font-size:12px;color:var(--dim)}
  .pw-advbtn{width:100%;height:44px;margin-top:14px;border-radius:12px;border:1px solid var(--line-2);background:var(--panel);color:var(--ink);font:650 14px/1 var(--font);cursor:pointer}
  .pw-advnote{font-size:12px;color:var(--dim);text-align:center;margin-top:6px}

  /* step 4 */
  .pw-orient{display:flex;gap:8px}
  .pw-orient button{flex:1;height:40px;border-radius:10px;border:1px solid var(--line-2);background:var(--panel);display:flex;align-items:center;justify-content:center;gap:8px;
    font:600 13px/1 var(--font);color:var(--dim);cursor:pointer}
  .pw-orient button.on{border-color:var(--accent);color:var(--accent);background:var(--accent-soft)}
  .pw-size{width:100%;height:46px;border:1px solid var(--line-2);border-radius:10px;padding:0 12px;font:16px var(--font);color:var(--ink);background:var(--panel)}
  .pw-note{font-size:12px;color:var(--dim);margin-top:6px}
  .pw-frames{display:grid;grid-template-columns:repeat(5,1fr);gap:6px}
  .pw-frames button{display:flex;flex-direction:column;align-items:center;gap:5px;padding:8px 2px 7px;border-radius:10px;border:1px solid var(--line-2);background:var(--panel);
    font:500 11px/1.2 var(--font);color:var(--dim);cursor:pointer}
  .pw-frames button.on{border-color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent);color:var(--ink);font-weight:650}
  .pw-frames button:disabled{opacity:.4;cursor:default}
  .pw-frames img,.pw-frames .none{width:40px;height:40px;border-radius:6px;object-fit:cover;display:block}
  .pw-frames .none{border:1px dashed var(--line-2);display:grid;place-items:center;font-size:10px;color:var(--faint)}
  .pw-facts{list-style:none;margin:14px 0 0;padding:0;font-size:12.5px;color:var(--dim);line-height:1.8}
  .pw-facts li::before{content:"\2713";color:#1e8e3e;margin-right:8px;font-weight:700}

  /* step 5 */
  .pw-order{display:flex;gap:14px;align-items:flex-start;margin:4px 0 12px}
  .pw-thumb{width:86px;flex:none;box-shadow:0 3px 12px rgba(20,30,45,.15);background:var(--hover);min-height:100px}
  .pw-thumb img{width:100%;height:auto;display:block}
  .pw-thumbwait{display:block;height:108px;animation:tpl-pulse 1.2s ease-in-out infinite;background:var(--hover)}
  .pw-order dl{display:grid;grid-template-columns:auto 1fr;gap:5px 12px;font-size:13px;margin:0}
  .pw-order dt{color:var(--dim)}
  .pw-order dd{margin:0;font-weight:600;min-width:0;overflow-wrap:anywhere}
  .pw-total{display:flex;justify-content:space-between;align-items:baseline;border-top:1px solid var(--line);padding-top:12px;font-size:15px}
  .pw-total b{font-size:20px}
  .pw-ship{font-size:12.5px;color:#1e8e3e;font-weight:600;margin-top:4px}
  .pw-fine{display:flex;align-items:center;gap:6px;flex-wrap:wrap;font-size:11.5px;color:var(--faint);margin-top:12px}
  .pw-fine[hidden]{display:none}
  .pw-attr{border:none;background:none;color:var(--faint);padding:0;display:inline-flex;cursor:pointer}
  .pw-digital{margin-top:14px;border:none;background:none;color:var(--accent);font:600 13px/1.3 var(--font);padding:0;cursor:pointer;text-align:left}
  .pw-digital[hidden]{display:none}
}
/* ?preview=1 (js/drafts.js): the page shows only the map or the poster, no
   app chrome. It's what the ChatGPT widget frames inline in the chat
   (planning/chatgpt-apps-plan.md); the visitor edits on the full site via
   the widget's own buttons. Read-only for the poster (nothing to select or
   drag); the map can still be panned and zoomed. Last in APP_CSS so these
   win the cascade. */
html.mb-preview body{--topH:0px;--railW:0px;--leftW:0px;--rightW:0px;--tabH:0px}
html.mb-preview .top,html.mb-preview .rail,html.mb-preview .left,html.mb-preview .right,
html.mb-preview .tabbar,html.mb-preview .designbar,html.mb-preview .maptools,
html.mb-preview .cornerbar,html.mb-preview #support-banner,html.mb-preview .fab-edit,
html.mb-preview .mobilemenu,html.mb-preview #toast,html.mb-preview .posterclip,
html.mb-preview .overlaybar,html.mb-preview .hamburger,html.mb-preview #setup-note{display:none!important}
html.mb-preview .canvas{top:0;left:0;right:0;bottom:0}
html.mb-preview .posterstage{pointer-events:none}
/* the data credit a preview carries (js/drafts.js paintAttribution) */
.preview-attrib{display:none}
html.mb-preview .preview-attrib{display:block;position:fixed;right:6px;bottom:4px;z-index:300;padding:1px 6px;border-radius:3px;
  background:rgba(255,255,255,.8);color:#4a525c;font:500 10.5px var(--font);pointer-events:none}
