  :root{
    --bg:#fbfaf8; --ink:#1f2933; --mut:#7b8794; --line:#e4e7eb;
    --acc:#1f2a44; --accsoft:#e9ecf3; --ok:#2f6b57; --oksoft:#e2eee8;
    --warn:#b7791f; --warnsoft:#fdf3e3;
    --danger:#b42318; --dangersoft:#fbeeec;
    --info:#2b6cb0; --infosoft:#e7f0fa;
    --terra:#b84a2e; --terrasoft:#f7e7df;
    /* Role tokens (#362): what a colour MEANS, so a theme re-values the
       meaning and never a selector. The default theme "ink" is the look
       before #362 — every role on the accent, done steps on --ok.
       docs/DESIGN-SYSTEM.md has the table; MAPORIES_THEME picks the block. */
    --live:var(--acc);       --livesoft:var(--accsoft);   /* running fills, spinners, the ticket ring */
    --done:var(--acc);       --donetick:var(--ok);        /* verdict badges; finished steps */
    --good:var(--acc);       --goodsoft:var(--accsoft);   /* good news: success, recommended, promo */
    --pin:var(--acc);                                     /* place marks in the chrome */
    --seltext:var(--acc);                                 /* text on a selected chip or tile */
    --cta:var(--acc);        --ctasoft:var(--accsoft);    /* the forward button of a scene; the My books pin + count */
    --chipfill:var(--ink);   --chipink:var(--bg);   --chipline:var(--ink);   /* pressed format chip */
    /* "Paper & ink" map palette (mockups/light-maps.html) — matches the
       printed maps in cartographer.py so screen and book agree */
    --card:#ffffff; --mapbg:#f4f2ec; --mapgrid:#e4dfd2; --maproute:#d97742;
    --mappin:#232a38; --maplbl:#5a6272; --mapmut:#a89f8d; --mapland:#eae4d6;
    /* the stage height: what the upload column needs at rest (ticket,
       five rows, filmstrip); every two-column work scene lands on it */
    --stageh:510px;
  }
  /* Passport: navy stays on controls; terracotta (the doc's runner-up
     accent, a deep cousin of the map route) carries life and good news */
  html[data-theme="passport"]{
    --live:var(--terra); --livesoft:var(--terrasoft);
    --done:var(--terra); --donetick:var(--terra);
    --good:var(--terra); --goodsoft:var(--terrasoft);
    --pin:var(--maproute); --seltext:var(--ink);
    --cta:var(--terra); --ctasoft:var(--terrasoft);
    --chipfill:var(--accsoft); --chipink:var(--ink); --chipline:var(--acc);
  }
  /* Meadow: the same split with the success green */
  html[data-theme="meadow"]{
    --live:var(--ok); --livesoft:var(--oksoft);
    --done:var(--ok); --donetick:var(--ok);
    --good:var(--ok); --goodsoft:var(--oksoft);
    --pin:var(--maproute); --seltext:var(--ink);
    --cta:var(--ok); --ctasoft:var(--oksoft);
    --chipfill:var(--accsoft); --chipink:var(--ink); --chipline:var(--acc);
  }
  *{box-sizing:border-box}
  [hidden]{display:none !important}  /* classes that set display would otherwise defeat the attribute */
  html,body{margin:0}
  body{background:var(--bg); color:var(--ink);
    font:14px/1.5 "Segoe UI",system-ui,-apple-system,sans-serif; min-height:100vh}
  button{font:inherit; cursor:pointer}
  :focus-visible{outline:2px solid var(--acc); outline-offset:2px}
  @media (prefers-reduced-motion: reduce){ *{animation:none!important; transition:none!important} }

  /* ---------- app bar ---------- */
  .appbar{display:flex; align-items:center; justify-content:space-between; gap:16px;
    padding:14px 26px; border-bottom:1px solid var(--line); background:var(--card);
    position:sticky; top:0; z-index:20; flex-wrap:wrap}
  .logo{display:flex; align-items:center; color:inherit; text-decoration:none}
  .logo img{height:26px; width:auto; display:block}
  .logo:hover img{opacity:.8}
  /* .account/.authbtn, .tripslink, #account/.avbtn/.menu/.mi and the
     sign-in dialog live in header.css (#504) — shared with the landing page */
  /* progress strip: bar above is identity, this row is progress; only shown
     while a book is open (subbar unhidden next to #newBook in app.js) */
  .subbar{display:flex; align-items:center; gap:22px; padding:0 26px;
    border-bottom:1px solid var(--line); background:var(--bg);
    font-size:12.5px; color:var(--mut); flex-wrap:wrap;
    position:sticky; top:var(--appbarh, 57px); z-index:19}
  .subbar .tripname{margin-left:auto; font-size:11.5px; color:var(--mut);
    overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:240px}
  /* phones: the strip is one row that scrolls sideways (go() keeps the
     current chip in view); no title line — the Design and Preview
     screens head with the title, the tab carries it everywhere else;
     New book lives on My books and in the account menu */
  @media (max-width:560px), (max-height:500px){
    .subbar{flex-wrap:nowrap; overflow-x:auto; gap:14px; padding:0 14px; scrollbar-width:none}
    .subbar::-webkit-scrollbar{display:none}
    .subbar .tripname{display:none}
    .appbar #newBook{display:none}
  }
  /* phone landscape: the bars scroll away with the page — a 390px-high
     viewport cannot hold them, the footer and a book */
  @media (max-height:500px){
    .appbar, .subbar{position:static}
  }
  /* My books home (USER-MANAGEMENT-PLAN.md phase 4b) — the "ledger":
     one row per book (mockups/mybooks-card-b-ledger.html). The thumb
     keeps the true cover ratio of the printed product (8×11" vertical);
     a ready book's thumb is a mini designed cover — photo + title band */
  #myBooks{margin:14px 0 4px; max-width:760px}
  .mbrow{background:var(--card); border:1px solid var(--line); border-radius:14px;
    overflow:hidden}
  .mbcard{display:flex; align-items:center; gap:14px; padding:12px 16px;
    text-decoration:none; color:inherit; border-bottom:1px solid var(--line)}
  .mbcard:last-child{border-bottom:0}
  .mbcard:hover{background:var(--accsoft)}
  .mbthumb{width:44px; aspect-ratio:8/11; border-radius:3px; flex-shrink:0;
    background:#eceae6 center/cover no-repeat; position:relative; overflow:hidden;
    box-shadow:0 1px 3px rgba(31,41,51,.25)}
  .mbthumb::before{content:""; position:absolute; inset:0 auto 0 0; width:3px;
    background:rgba(0,0,0,.18)}
  /* a book's thumb is a blank paper tile until its real cover renders
     (realCovers); the photo stand-in only shows when there is no cover
     to render — so nothing wrong-looking flashes before the right thing */
  .mbthumb.ready{display:flex; flex-direction:column; justify-content:flex-end;
    background:#e9e4d8}
  .mbthumb.ready i{display:block; height:62%; background:#5a6272 center/cover no-repeat;
    opacity:0; transition:opacity .25s}
  .mbthumb.ready b{display:block; font:italic 6px/1.6 Georgia,serif; color:#f2efe8;
    padding:0 3px 3px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
    opacity:0; transition:opacity .25s}
  .mbthumb.ready.fallback{background:#20313b}
  .mbthumb.ready.fallback i, .mbthumb.ready.fallback b{opacity:1}
  /* the real cover (realCovers): the coverThumb miniature sizes itself, so
     the box just wraps it — no mock band, no spine, no wash */
  .mbthumb.realcover{width:auto; height:auto; aspect-ratio:auto; background:none;
    box-shadow:none; display:block}
  .mbthumb.realcover::before{content:none}
  /* the real cover fades over the stand-in instead of popping */
  .mbthumb.realcover{animation:coverin .25s ease-out}
  @keyframes coverin{from{opacity:0}to{opacity:1}}
  @media (prefers-reduced-motion: reduce){ .mbthumb.realcover{animation:none} }
  /* hover preview (hoverPreviews): the cover big, floating beside the thumb */
  #mbPop{position:fixed; z-index:60; padding:10px; border-radius:10px; background:#fff;
    box-shadow:0 8px 30px rgba(31,41,51,.28), 0 0 0 1px rgba(31,41,51,.08);
    pointer-events:none; animation:mbpop .16s ease-out}
  @keyframes mbpop{from{opacity:0; transform:translateX(-6px)} to{opacity:1; transform:none}}
  .mbpop-photo{display:block; aspect-ratio:8/11; border-radius:3px;
    background:#eceae6 center/cover no-repeat}
  .mbwho{flex:1; min-width:0}
  .mbtitle{display:block; font-size:13.5px; font-weight:600; overflow:hidden;
    text-overflow:ellipsis; white-space:nowrap}
  .mbmeta{display:block; font-size:11.5px; color:var(--mut)}
  .mbbadge{font-size:10.5px; font-weight:700; letter-spacing:.03em; padding:3px 10px;
    border-radius:999px; flex-shrink:0; width:104px; text-align:center; white-space:nowrap}
  .b-ready{background:var(--oksoft); color:var(--ok)}
  .b-review{background:var(--infosoft); color:var(--info)}
  .b-prog{background:var(--warnsoft); color:var(--warn)}
  .mbwhen{width:110px; text-align:right; font-size:11.5px; color:var(--mut); flex-shrink:0}
  .mbupd{display:none}             /* narrow screens: recency joins the meta line */
  .mbgo{color:var(--mut); flex-shrink:0}
  /* #207: per-card delete for unfinished books; press-again confirm */
  .mbdel{font:inherit; font-size:12px; line-height:1; padding:6px 8px; border-radius:999px;
    border:1px solid transparent; background:transparent; color:var(--mut); cursor:pointer;
    flex-shrink:0; opacity:0; transition:opacity .12s}
  .mbcard:hover .mbdel, .mbdel.armed, .mbdel:focus-visible{opacity:1}
  .mbdel:hover, .mbdel.armed{border-color:var(--danger); background:var(--dangersoft); color:var(--danger)}
  .mbdel.armed{font-size:11px; font-weight:600}
  /* a book with an order can't be deleted: the slot stays so every row's
     columns line up, the button itself is out of sight and tab order */
  .mbdel.locked{visibility:hidden; pointer-events:none}
  @media (hover:none){ .mbdel{opacity:1} }
  .mbcard:hover .mbgo{color:var(--acc)}
  /* phone: the row becomes a three-line card — state first as a coloured
     eyebrow, then the title (wrapping, full width), then photos · pages ·
     recency; delete and the chevron stack on the right */
  @media (max-width:560px){
    .mbcard{display:grid; grid-template-columns:auto 1fr auto; grid-template-rows:auto auto;
      column-gap:12px; row-gap:2px; align-items:start}
    .mbthumb{grid-area:1/1/3/2; align-self:center}
    /* .mbcard prefix: the state colour classes (.b-order lives further
       down) must not win the fill back */
    .mbcard .mbbadge{grid-area:1/2; justify-self:start; width:auto; padding:0; background:none;
      font-size:11px; letter-spacing:.04em; display:inline-flex; align-items:center; gap:6px}
    .mbcard .mbbadge::before{content:""; width:6px; height:6px; border-radius:50%; background:currentColor}
    .mbwho{grid-area:2/2}
    .mbtitle{white-space:normal; line-height:1.3}
    .mbwhen{display:none}
    .mbupd{display:inline}
    .mbdel{grid-area:1/3; justify-self:end; opacity:1}
    .mbgo{grid-area:2/3; justify-self:end; align-self:center}
  }
  /* underline tabs: flat labels sitting on the strip's own bottom border;
     the current step underlines in accent and takes ink weight, a done
     step carries a small tick in accent, steps ahead stay muted */
  .step{display:inline-flex; align-items:center; gap:6px; padding:9px 4px 8px;
    border:0; border-bottom:2px solid transparent; margin-bottom:-1px;
    background:transparent; color:inherit; font-weight:500}
  /* the mark slot holds the ✓ or the spinner; the digit is not shown */
  .step .mark{display:none}
  .step.done .mark, .step.busy .mark{display:inline-flex; align-items:center}
  .step.done .mark svg{width:12px; height:12px; display:block}
  .step.done{color:var(--donetick)}
  .step.done:hover{color:var(--ink)}
  .step.cur{color:var(--ink); font-weight:650; border-bottom-color:var(--acc)}
  .step:disabled{cursor:default}
  /* the wizard behind an open order: greyed out, no accent, no hover */
  .step.locked, .step.locked.done{color:var(--mut); opacity:.55}
  .step.locked:hover{color:var(--mut)}
  /* work in flight: the mark slot spins (upload/prepare/locate/analyze on
     Upload, design/print on Book) — visible from any step */
  .step.busy .mark{width:.9em; height:.9em; border:2px solid var(--line);
    border-top-color:var(--live); border-radius:50%;
    animation:spin .8s linear infinite}
  .step.busy{color:var(--live)}
  @media (prefers-reduced-motion: reduce){ .step.busy .mark{animation-duration:2.5s} }

  /* bottom padding tracks the fixed bar's measured height (--barh from JS)
     so a wrapped three-row bar on a phone never covers the content */
  /* #477: the one-time welcome card; a stage's width and gutters, the
     stage below keeps its own top padding so the card reads as a banner */
  .hello{display:grid; grid-template-columns:auto 1fr auto; gap:14px; align-items:start;
    max-width:896px; margin:22px auto -10px; box-sizing:border-box;
    background:var(--card); border:1px solid var(--line); border-left:4px solid var(--cta);
    border-radius:12px; padding:14px 16px}
  body.booting .hello{display:none}
  .hello .mk{flex:none}
  .hello h3{margin:0 0 2px; font-size:15px; letter-spacing:-.01em}
  .hello p{margin:0; font-size:13px; color:var(--mut); max-width:64ch}
  .hello p b{color:var(--ink); font-weight:600}
  .hello .hellomail{margin-top:8px; font-size:12px; color:var(--mut)}
  .hellox{border:0; background:transparent; color:var(--mut); width:28px; height:28px;
    border-radius:50%; font-size:15px; line-height:1}
  .hellox:hover{background:var(--bg); color:var(--ink)}
  @media (max-width:640px){
    .hello{grid-template-columns:auto 1fr; margin:14px 14px -10px}
    .hello .hellox{grid-column:2; justify-self:end; margin-top:-4px}
  }
  .stage{display:none; max-width:940px; margin:0 auto;
    padding:28px 22px calc(var(--barh, 90px) + 30px)}
  .stage.on{display:block}
  /* boot: no stage paints until resumeTrip() has picked one — otherwise
     the default upload view flashes for the trips-fetch round trip
     before the welcome-back gate replaces it */
  body.booting .stage{display:none}
  h2{font-size:22px; margin:0 0 4px; letter-spacing:-.01em; text-wrap:balance}
  /* the work scenes have no sub line under the heading (the ticket says
     what is happening): the columns need their own breathing room */
  #sceneProgress h2, #sceneAnalyze h2, #sceneWords h2, #sceneLayout h2{margin-bottom:16px}
  .sub{color:var(--mut); margin:0 0 20px; font-size:13.5px; max-width:64ch}
  .btn{display:inline-block; background:var(--cta); color:#fff; border:0; border-radius:10px;
    padding:10px 20px; font-weight:600; font-size:14px}
  .btn:disabled{background:#c3cbd4; cursor:default}
  .btn.ghost{background:var(--card); color:var(--acc); border:1px solid var(--acc)}
  .btn.ghost:disabled{color:var(--mut); border-color:var(--line); background:var(--card)}
  .btn.quiet{background:transparent; color:var(--mut); border:0; font-weight:500}
  .panel{background:var(--card); border:1px solid var(--line); border-radius:12px; padding:16px}
  .panel h3{margin:0 0 10px; font-size:12.5px; text-transform:uppercase; letter-spacing:.08em;
    color:var(--mut); font-weight:600}
  .hint{color:var(--mut); font-size:12.5px; margin-top:12px}
  .hint b{color:var(--ink)}

  /* ---------- photo placeholders ---------- */
  .photo{border-radius:6px; position:relative; overflow:hidden; background:#888}
  /* #277: the Storyteller's title, filled in while the review is open */
  .photo .ttl{position:absolute; left:8px; right:8px; bottom:22px; font-size:11px; line-height:1.2;
    color:#fff; text-shadow:0 1px 2px rgba(0,0,0,.6); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
    font-style:italic; pointer-events:none; z-index:2}
  .photo .ttl:empty{display:none}
  .titlebox{display:inline-flex; align-items:center; gap:6px; position:relative; max-width:100%}
  .titlebox .booktitle{min-width:0}
  .titlebox .titlecaret{flex:none}
  .titlecaret{font:inherit; font-size:18px; line-height:1; border:1px solid var(--line); background:var(--card);
    color:inherit; border-radius:6px; padding:2px 8px; cursor:pointer}
  .titlecaret[aria-expanded="true"]{border-color:var(--acc); color:var(--acc)}
  .titlemenu{position:absolute; z-index:5; margin-top:4px; min-width:280px; max-width:520px; background:var(--card);
    border:1px solid var(--line); border-radius:8px; box-shadow:0 8px 24px rgba(0,0,0,.12); padding:6px}
  .titlemenu .opt{display:flex; justify-content:space-between; gap:12px; width:100%; text-align:left; font:inherit; font-size:14px;
    padding:8px 10px; border:0; border-radius:6px; background:transparent; color:inherit; cursor:pointer}
  .titlemenu .opt:hover, .titlemenu .opt:focus-visible{background:var(--accsoft); outline:none}
  .titlemenu .opt[aria-selected="true"]{color:var(--acc)}
  .titlemenu .opt .tick{opacity:0} .titlemenu .opt[aria-selected="true"] .tick{opacity:1}
  .titlemenu .hint{border-top:1px solid var(--line); margin-top:4px; padding:8px 10px 4px; font-size:12px; color:var(--mut)}
  .filmstrip.aside .fthumb{opacity:.45}   /* set aside: dimmed, like on the review */
  .photo::after{content:""; position:absolute; inset:0;
    background:linear-gradient(160deg,rgba(255,255,255,.18),rgba(0,0,0,.22))}
  .g1{background:linear-gradient(135deg,#c9a36a,#7a5b3a)} .g2{background:linear-gradient(135deg,#7fa8b8,#31586b)}
  .g3{background:linear-gradient(135deg,#d0765a,#8a3d2e)} .g4{background:linear-gradient(135deg,#9db07f,#4f6b3f)}
  .g5{background:linear-gradient(135deg,#c8c2b4,#6e6a5e)} .g6{background:linear-gradient(135deg,#b48ab0,#5f4670)}
  .g7{background:linear-gradient(135deg,#e0b45c,#a06a28)} .g8{background:linear-gradient(135deg,#6f9e97,#2f5a54)}
  .g9{background:linear-gradient(135deg,#a9b6c9,#5a6b85)} .g10{background:linear-gradient(135deg,#d9a3a3,#8a4949)}
  .g11{background:linear-gradient(135deg,#8fb3d9,#3b5a80)} .g12{background:linear-gradient(135deg,#cdbb7a,#7d6c2f)}

  /* ---------- step 1: upload ---------- */
  /* #318: the columns are fixed shares, and the rows start-aligned — one
     column's content never resizes the other, so a side panel keeps the
     same box on every scene and while cards arrive */
  .upgrid{display:grid; grid-template-columns:1.12fr .88fr; gap:18px; align-items:start}
  /* with the map beside it the row stretches: both panels end on the same
     line, the map box takes the slack (see .mapslot below) */
  .upgrid:has(> .mapslot > .mappanel){align-items:stretch}
  .upgrid>*{min-width:0}  /* let wide children (filmstrip) scroll instead of stretching the column */
  /* the photo column is a flex column so its panel fills the grid row:
     the same height as the map panel beside it from the first moment,
     not the content's own height that grows when the filmstrip fills */
  .upcol{display:flex; flex-direction:column}
  .upcol > .panel{flex:1 1 auto}
  @media (max-width:760px){ .upgrid{grid-template-columns:1fr} }
  .drop{border:2px dashed var(--line); border-radius:14px; background:var(--card);
    padding:28px 18px; text-align:center; cursor:pointer}
  /* drop scene: the zone is the whole screen's one action */
  .drop.hero{padding:44px 24px 36px; max-width:640px; margin:14px auto 0;
    display:flex; flex-direction:column; align-items:center; gap:12px}
  .drop.hero .hint{margin:0}
  .drop.hero .hint.warn{color:var(--warn)}
  .drop.hero .upicon{width:28px; height:28px; fill:none; stroke:var(--acc); stroke-width:1.6;
    stroke-linecap:round; stroke-linejoin:round}
  /* import sources under the hero: a rule, then one tile per source */
  .orrule{display:flex; align-items:center; gap:12px; max-width:640px; margin:22px auto 10px;
    color:var(--mut); font-size:11.5px; text-transform:uppercase; letter-spacing:.08em; font-weight:600}
  .orrule::before, .orrule::after{content:""; flex:1; height:1px; background:var(--line)}
  .sources{display:grid; grid-template-columns:repeat(2, minmax(0,1fr)); gap:10px; max-width:640px; margin:0 auto}
  .src{display:flex; align-items:center; gap:10px; min-height:48px; padding:10px 14px; text-align:left;
    border:1px solid var(--line); border-radius:12px; background:var(--card); color:var(--ink);
    font-size:13px; font-weight:600}
  .src svg{width:20px; height:20px; flex:none; fill:none; stroke:currentColor; stroke-width:1.6;
    stroke-linecap:round; stroke-linejoin:round}
  .src span{flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
  .src i{font-style:normal; font-size:10.5px; font-weight:600; color:var(--mut); background:var(--bg);
    border:1px solid var(--line); border-radius:999px; padding:1px 7px}
  .src:not([disabled]):hover{border-color:var(--acc); background:var(--accsoft)}
  .src[disabled]{color:var(--mut); cursor:default}
  @media (max-width:520px){
    .drop.hero{padding:30px 18px 26px}
    .sources{grid-template-columns:1fr}
  }
  /* one screen per moment: the incoming stage/scene slides up into place */
  @keyframes scenein{from{opacity:0; transform:translateY(18px)} to{opacity:1; transform:none}}
  .stage.on, .scene:not([hidden]){animation:scenein .32s ease-out}
  @media (prefers-reduced-motion: reduce){ .stage.on, .scene:not([hidden]){animation:none} }
  /* five two-line rows show (52px each), the rest scroll; the list draws
     the rule under its window itself, so it sits below the fifth row
     whatever the scroll — the last row's own rule is dropped so the end
     of the list does not show two (max-height is border-box: 260 + 1) */
  #upList{max-height:261px; overflow-y:auto; scrollbar-width:thin; border-bottom:1px solid var(--line)}
  /* in-flight rows wash left→right in the soft accent (--p from rowHTML);
     done rows have no --p, so the wash collapses to transparent */
  .uprow{display:flex; align-items:center; gap:10px; padding:7px 0;
    font-size:13px; border-bottom:1px solid var(--line); border-radius:6px;
    background:linear-gradient(90deg, var(--accsoft) calc(var(--p,0)*1%), transparent 0)}
  .uprow:last-child{border-bottom:0}   /* the list's own rule closes it */
  .thumbsq{width:34px; height:34px; flex:none}
  /* finished photos carry their verdict on the thumb corner */
  .thumbwrap{position:relative; flex:none; width:34px; height:34px}
  .thumbwrap .thumbsq{display:block; width:100%; height:100%}
  .badge{position:absolute; right:-4px; bottom:-4px; width:16px; height:16px;
    border-radius:50%; border:2px solid var(--card); background:var(--done);
    display:grid; place-items:center; color:#fff; font-size:9px; font-weight:700; line-height:1}
  .badge.off{background:#9aa5b1}
  .badge.off svg{width:9px; height:9px; color:#fff}
  /* mapless photos read at a glance: washed-out thumb + grey pin badge */
  .uprow.noloc .thumbsq{filter:grayscale(.8) opacity(.5)}
  /* filename and place stack in one flexible column so a long place name
     gets the row's full width instead of squeezing the filename */
  .uprow .lines{flex:1; min-width:0; display:flex; flex-direction:column; gap:1px}
  .fill{min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
  .meta{color:var(--mut); font-size:12px; font-variant-numeric:tabular-nums;
    display:inline-flex; align-items:center; gap:4px; min-width:0; max-width:100%}
  .meta .txt{overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
  /* provisional place label from the cache peek — upgraded by the real answer */
  .meta .txt.approx{font-style:italic; opacity:.75}
  .lpin{width:12px; height:12px; flex:none; color:var(--pin)}
  .lpin.none{color:#9aa5b1}
  .prog{height:5px; border-radius:3px; background:var(--line); overflow:hidden; width:70px; flex:none}
  .prog i{display:block; height:100%; width:0; background:var(--live)}
  .ok{color:var(--donetick); font-weight:700; width:16px; text-align:center}
  .uprow .rm{flex:none; width:22px; height:22px; border:0; border-radius:50%;
    background:none; color:var(--mut); font-size:13px; line-height:1; cursor:pointer}
  .uprow .rm:hover{background:var(--warnsoft); color:var(--warn)}
  /* a re-upload of the same bytes: the row fades under a rubber stamp for a
     beat, then leaves the table (the photo itself stays server-side, visible
     under review's "Dropped" filter) */
  .uprow.dup{position:relative}
  .uprow.dup .thumbwrap, .uprow.dup .lines{opacity:.45}
  .stamp{position:absolute; right:10px; top:50%; pointer-events:none;
    transform:translateY(-50%) rotate(-8deg);
    border:2px solid var(--warn); color:var(--warn); border-radius:4px;
    padding:1px 6px; font-size:9.5px; font-weight:800; letter-spacing:.14em;
    background:var(--warnsoft); animation:stampIn .18s ease-out}
  @keyframes stampIn{
    from{transform:translateY(-50%) rotate(-8deg) scale(1.7); opacity:0}}
  .chip{border-radius:8px; padding:8px 11px; font-size:12.5px; margin-top:12px}
  .chip.warn{background:var(--warnsoft); color:var(--warn)}
  .chip.good{background:var(--oksoft); color:var(--ok)}
  .chip button{background:none; border:0; color:inherit; text-decoration:underline; padding:0; font-size:inherit}
  /* the staging "work ticket": one raised full-width header strip capping
     the panel — status row on top, segment bar stacked inside it below */
  .ticket{margin-bottom:12px}
  .segwrap{margin-top:11px}
  .panel .ticket.idle h3{color:var(--mut); font-weight:500}
  /* header strip bleeds to the panel's edges and sits slightly raised;
     running lifts it further with an accent-tinted glow, idle lies flat */
  .runhead{margin:-16px -16px 0; padding:12px 16px; border-radius:12px 12px 0 0;
    background:linear-gradient(180deg, var(--card), #f6f4f0);
    border-bottom:1px solid var(--line);
    box-shadow:inset 0 1px 0 rgba(255,255,255,.9), 0 2px 5px rgba(31,41,51,.05);
    transition:background .3s, box-shadow .3s, border-color .3s}
  .headrow{display:flex; align-items:center; gap:14px}
  .ticket.idle .runhead{background:var(--card); box-shadow:none}
  .ticket.running .runhead{
    background:linear-gradient(180deg, #ffffff 30%, var(--accsoft));
    border-bottom-color:var(--accsoft);
    box-shadow:inset 0 1px 0 rgba(255,255,255,.9), 0 5px 14px rgba(31,42,68,.14)}
  /* the text block is the ring's height, whatever it says: one line
     each, clipped with an ellipsis, a hidden sub or an extra note never
     moves the ticket (and so never moves the stage) */
  .headrow .rtxt{flex:1; min-width:0; height:64px; overflow:hidden;
    display:flex; flex-direction:column; justify-content:center}
  .headrow .rtxt > *{white-space:nowrap; overflow:hidden; text-overflow:ellipsis; flex:none}
  .rtxt{flex:1; min-width:0}
  .panel .ticket h3{font-size:14px; text-transform:none; letter-spacing:.01em;
    color:var(--ink); margin:0; font-weight:600}
  /* a ticket that is not the panel's cap (order summary, the book desk):
     its own bordered strip instead of the bleed */
  .ticket.inline{margin:14px 0 0}
  .ticket.inline .runhead{margin:0; border-radius:10px; border:1px solid var(--line)}
  .ticket.inline.running .runhead{border-color:var(--accsoft)}
  .rsub{color:var(--mut); font-size:12px; margin-top:1px}
  /* the ticket's note line (photos without location): head + sub + note
     stay inside the 64px ring, so it adds no height */
  .rnote{color:var(--warn); font-size:12px; line-height:1.3; margin-top:1px}
  .rnote .lpin{color:var(--warn); vertical-align:-2px}
  .rnote b{font-weight:600}
  .rnote button{background:none; border:0; padding:0; font:inherit; color:inherit; text-decoration:underline; cursor:pointer}
  .rnote button[aria-pressed="true"]{font-weight:600}
  .rtime{flex:none; text-align:right; color:var(--mut);
    font:12.5px/1.3 ui-monospace,SFMono-Regular,Menlo,monospace}
  .rtime b{display:block; font-size:16px; color:var(--ink); font-weight:600;
    font-variant-numeric:tabular-nums}
  .rtime i{font-style:normal; font-size:10.5px; text-transform:uppercase; letter-spacing:.08em}
  .ring{position:relative; flex:none; width:64px; height:64px; border-radius:50%;
    display:grid; place-items:center;
    background:conic-gradient(var(--live) calc(var(--p,0)*1%), var(--line) 0)}
  .ring::before{content:""; position:absolute; inset:6px; border-radius:50%;
    background:var(--card)}
  .ring b{position:relative; font-size:14px; font-variant-numeric:tabular-nums}
  .ring::after{content:""; position:absolute; inset:-7px; border-radius:50%;
    border:2px dashed var(--live); opacity:0; transition:opacity .3s}
  .ticket.running .ring::after{opacity:.45; animation:ringrot 2.4s linear infinite}
  /* indeterminate (Analyze): no % and no conic fill — the dashed halo
     above is the only motion, saying "running" without claiming progress */
  .ring.indet{background:var(--line)}
  .ring.indet b{visibility:hidden}
  @keyframes ringrot{to{transform:rotate(360deg)}}
  .ticket .segbar{margin:0 0 4px}
  .ticket .seglabels{margin-bottom:0}
  /* phones: keep the elapsed clock — compact the head, the strip still
     bleeds to the panel's edges */
  @media (max-width:560px){
    .runhead{padding:10px 12px}
    .headrow{gap:10px}
    .ring{width:48px; height:48px}
    .ring b{font-size:12px}
    .rtime b{font-size:14px}
    .headrow .rtxt{height:48px}
    .rsub, .rnote{font-size:11px; line-height:1.2; margin-top:0}
  }
  .segbar{display:flex; gap:4px; height:9px; margin:2px 0 6px; transition:opacity .4s}
  .segbar.done, .ticket.settled .segbar{opacity:.55}
  .seg{flex:1; background:var(--line); border-radius:5px; overflow:hidden}
  .seg i{display:block; height:100%; width:0; background:var(--live); transition:width .25s linear}
  .seg.run i{background-image:linear-gradient(45deg, rgba(255,255,255,.28) 25%, transparent 25%,
    transparent 50%, rgba(255,255,255,.28) 50%, rgba(255,255,255,.28) 75%, transparent 75%);
    background-size:14px 14px; animation:barber .6s linear infinite}
  @keyframes barber{to{background-position:14px 0}}
  /* Analyze runs indeterminate: an activity strip, not a fraction — soft
     accent stripes sweeping across the full segment until the pass lands */
  .seg.indet i, .seg.indet.run i{background-color:var(--livesoft);
    background-image:linear-gradient(45deg, var(--live) 25%, transparent 25%,
      transparent 50%, var(--live) 50%, var(--live) 75%, transparent 75%);
    background-size:14px 14px; opacity:.45; animation:barber .8s linear infinite}
  #upHead .pct{margin-left:auto; color:var(--mut); font-weight:400}
  .seglabels{display:flex; gap:4px; font-size:10px; color:var(--mut);
    text-transform:uppercase; letter-spacing:.05em; margin-bottom:10px}
  .seglabels span{flex:1; text-align:center}
  .seglabels span.cur{color:var(--live); font-weight:700}
  .segbar[hidden], .seglabels[hidden], .btn[hidden]{display:none}
  .seg[hidden], .seglabels span[hidden]{display:none}
  img.flag{width:18px; height:auto; border-radius:2px; vertical-align:-2px;
    box-shadow:0 0 0 1px rgba(0,0,0,.08)}
  /* the "mapory" frame: every photo the same square polaroid-shaped card */
  #photoDlg{border:0; border-radius:6px; padding:18px 18px 14px;
    width:min(560px, 92vw, calc(92vh - 120px)); background:var(--card); color:var(--ink);
    box-shadow:0 24px 60px rgba(0,0,0,.35)}
  #photoDlg::backdrop{background:rgba(16,32,44,.55)}
  #dlgImg{display:block; width:100%; aspect-ratio:1; object-fit:cover; background:var(--mapbg)}
  .dlgmeta{padding:12px 4px 4px; text-align:center}
  .dlgmeta h3{margin:0 0 4px; font-size:14px}
  #dlgLines div{color:var(--mut); font-size:12.5px; line-height:1.7}
  #dlgLines .dlgwhy{color:var(--warn)}
  #dlgLines .shp{vertical-align:-3.5px}
  .dlgclose{position:absolute; top:8px; right:10px; border:0; z-index:2;
    background:rgba(255,255,255,.85); width:28px; height:28px; border-radius:50%; font-size:16px}
  /* photo-to-photo arrows: the touch twin of the arrow keys */
  .dlgnav{position:absolute; top:50%; transform:translateY(-50%); z-index:2;
    border:0; width:40px; height:40px; border-radius:50%;
    background:rgba(255,255,255,.85); box-shadow:0 1px 6px rgba(0,0,0,.25);
    font-size:22px; line-height:1; color:var(--ink)}
  .dlgnav.prev{left:8px}
  .dlgnav.next{right:8px}
  .uprow{cursor:pointer}
  .uprow:hover{background:linear-gradient(90deg, var(--accsoft) calc(var(--p,0)*1%), var(--bg) 0)}
  .fthumb{cursor:pointer}
  .cchips{display:flex; flex-wrap:wrap; gap:6px; margin-top:10px}
  .cchip{display:flex; align-items:center; gap:6px; padding:4px 11px; font-size:12px;
    border:1px solid var(--line); border-radius:999px; background:var(--card); color:var(--mut)}
  .cchip[aria-pressed="true"]{border-color:var(--acc); color:var(--seltext);
    background:var(--accsoft); font-weight:600}
  /* one row: names fold away, the pressed country keeps its name */
  .cchips.compact .cchip:not([aria-pressed="true"]) .cname{display:none}
  .cchips.compact .cchip:not([aria-pressed="true"]){padding:4px 8px}
  .spin{display:inline-block; width:12px; height:12px; border:2px solid var(--line);
    border-top-color:var(--live); border-radius:50%; animation:spin .8s linear infinite;
    vertical-align:-1px; margin-right:7px}
  @keyframes spin{to{transform:rotate(360deg)}}
  @media (prefers-reduced-motion: reduce){ .spin{animation-duration:2.5s} }
  .filmstrip{display:flex; gap:6px; overflow-x:auto; margin-top:12px; padding-bottom:3px; scrollbar-width:thin}
  /* the upload strip stays in flow from the start, its row reserved
     (tile + padding), so the first tile pops into space the panel
     already has instead of resizing it */
  #filmstrip{min-height:63px}
  .fthumb{width:60px; height:60px; flex:none; border-radius:6px; background:var(--line) center/cover no-repeat}
  .fthumb.pop{animation:pop .25s ease-out}
  @keyframes pop{from{opacity:0; transform:scale(.85)}}
  @media (prefers-reduced-motion: reduce){ .fthumb.shimmer{animation:none} }
  /* the mapless filter: only the rows with the grey pin stay */
  #upList.mapless .uprow:not(.noloc){display:none}
  .maphead{display:flex; align-items:center; justify-content:space-between; gap:10px; margin-bottom:10px}
  .maphead h3{margin:0}
  .mapbtns{display:flex; gap:6px; flex:none}
  .maphead .pstat{flex:1; font-size:13px; color:var(--mut)}
  /* the count: pin + number, crossed pin + number; never wraps */
  .maphead .pstat{display:flex; align-items:center; gap:10px; white-space:nowrap; font-variant-numeric:tabular-nums}
  .maphead .pc{display:inline-flex; align-items:center; gap:3px}
  .maphead .pc.none{color:#9aa5b1}
  .mapbtn{display:inline-flex; align-items:center; gap:6px; padding:6px 12px;
    border:1px solid var(--line); border-radius:999px; background:var(--card); color:var(--mut);
    font-size:12px; cursor:pointer}
  .mapbtn svg{width:14px; height:14px}
  .mapbtn.icon{padding:0; width:30px; height:30px; justify-content:center}
  .mapbtn.icon svg{width:16px; height:16px}
  .mapbtn.has{border-color:var(--acc); color:var(--seltext)}
  .mapbtn[aria-pressed="true"]{background:var(--accsoft); color:var(--seltext); border-color:var(--acc)}
  .mapbtn:disabled{opacity:.5; cursor:default}
  /* the map panel travels between scenes (mountMap): square in a side
     column, a wide strip above the review grid */
  .mapslot:empty{display:none}
  .mapslot.wide{margin:0 0 16px}
  /* in a side slot the map box fills the panel, and the panel is the same
     height on upload, analyze and words: the floor (--stageh less the
     panel's chrome) is what the upload column needs at rest — ticket, five rows, filmstrip — so the
     analyze and words panels, whose ledgers start empty, stretch to the
     same line; a taller photo column (a chip row) grows the row and the
     box with it. The dots drawing re-fits the box whenever it changes
     size (drawMapSvg via the ResizeObserver). */
  .upgrid > .mapslot{display:flex}
  .mapslot:not(.wide) .mappanel{flex:1; display:flex; flex-direction:column}
  .mapslot:not(.wide) .mapbox, .mapslot:not(.wide) .mapwrap{flex:1 1 0; min-height:calc(var(--stageh) - 112px); height:auto}  /* 112 = the panel's chrome round the box; basis 0: the SVG's own aspect must not size the row */
  /* the review strip draws at the same box as the widened upload map, so
     the map keeps one aspect from scene to scene */
  .mappanel.wide .mapbox, .mappanel.wide .mapwrap{height:calc(var(--stageh) - 112px)}
  /* in a side slot the printed map widens: the slot spans both columns
     on the same grid row as the photo column, so it lays over that
     column's exact box and still sizes the row — the taller of the two
     wins and the panel never outgrows its border (it stays in flow;
     absolute positioning let the row shrink to a short ledger). The
     panel grows out of its own column toward the left; the toggle
     brings it back */
  .upgrid > :first-child{grid-row:1; grid-column:1}
  .upgrid > .mapslot:has(> .mappanel.big){grid-row:1; grid-column:1 / -1; z-index:2}
  .mapslot:not(.wide) .mappanel.big{transform-origin:100% 50%; animation:mapin .3s ease-out}
  @keyframes mapin{from{opacity:0; transform:scaleX(.45)} to{opacity:1; transform:none}}
  @media (prefers-reduced-motion: reduce){ .mapslot:not(.wide) .mappanel.big{animation:none} }
  /* the map view: the server's Passport SVG, drawn at the box's own aspect */
  .mapwrap{background:var(--mapbg); border-radius:8px; overflow:hidden;
    display:flex; align-items:center; justify-content:center}
  .mapwrap{position:relative; cursor:grab; touch-action:none; user-select:none}
  .mapwrap.dragging{cursor:grabbing}
  /* photo points (map.svg?photos=1): a point is a target; the hovered one
     goes solid, its card sits beside it; the click lands on the photo's
     row or tile, which flashes once */
  .mapwrap .map-photo{cursor:pointer; transition:fill-opacity .12s}
  .mapwrap .map-photo:hover, .mapwrap .map-photo.hot{fill-opacity:1}
  .mapwrap .map-group{cursor:pointer}
  .mapwrap .map-group:hover circle, .mapwrap .map-group.hot circle{stroke-width:.9}
  .mapwrap .map-group:hover text, .mapwrap .map-group.hot text{font-weight:700}
  .mapcard{position:absolute; z-index:2; display:flex; align-items:center; gap:8px; padding:5px 9px 5px 5px;
    background:var(--card); border:1px solid var(--line); border-radius:8px; pointer-events:none;
    box-shadow:0 6px 18px rgba(31,41,51,.14); max-width:220px}
  .mapcard[hidden]{display:none}
  .mapcard .thumbsq{width:40px; height:40px; flex:none; border-radius:5px; background:var(--accsoft) center/cover}
  .mapcard .lines{display:flex; flex-direction:column; min-width:0; font-size:12px; line-height:1.25}
  .mapcard b{font-weight:600; color:var(--ink); white-space:nowrap; overflow:hidden; text-overflow:ellipsis}
  .mapcard span:not(.thumbsq){color:var(--mut); white-space:nowrap; overflow:hidden; text-overflow:ellipsis}
  .mapflash{animation:mapflash 1.4s ease-out}
  @keyframes mapflash{0%,60%{box-shadow:0 0 0 3px var(--acc)} 100%{box-shadow:0 0 0 0 transparent}}
  @media (prefers-reduced-motion: reduce){ .mapflash{animation:none; box-shadow:0 0 0 3px var(--acc)} }
  .mapsvg{position:absolute; inset:0; display:flex; align-items:center; justify-content:center}
  .mapwrap svg{width:100%; height:100%; display:block}
  .mapctl{position:absolute; right:10px; top:10px; display:flex; flex-direction:column; gap:4px}
  .mapctl button{width:28px; height:28px; border:1px solid var(--mapgrid); border-radius:6px;
    background:var(--card); color:var(--ink); font-size:14px; line-height:1; cursor:pointer}
  .mapctl button#mzFit{font-size:10px; letter-spacing:.04em}
  .mapctl button:hover{border-color:var(--acc); color:var(--acc)}
  /* the ticket pill on the widened map: only there, only while its ticket
     is running or just landed (an idle ticket has nothing to say) */
  .mapmini{position:absolute; left:10px; top:10px; z-index:1; display:flex; align-items:center; gap:9px;
    height:36px; padding:0 12px 0 5px; max-width:calc(100% - 60px); box-sizing:border-box;
    border:1px solid var(--mapgrid); border-radius:9px; background:var(--card); color:var(--ink);
    cursor:pointer; box-shadow:0 1px 4px rgba(31,41,51,.08)}
  .mapmini:hover{border-color:var(--acc)}
  .mapmini:not(.running):not(.settled), .mappanel:not(.big) .mapmini{display:none}
  .mapmini .ring{width:26px; height:26px}
  .mapmini .ring::before{inset:3px}
  .mapmini .ring::after{inset:-4px; border-width:1.5px}
  .mapmini.running .ring::after{opacity:.45; animation:ringrot 2.4s linear infinite}
  .mapmini .ring b{display:none}   /* the fill is the number at this size */
  .mapmini h3{margin:0; font-size:12.5px; font-weight:600; letter-spacing:.01em; text-transform:none;
    white-space:nowrap; overflow:hidden; text-overflow:ellipsis; min-width:0}
  .mapmini .segbar{flex:none; width:96px; height:4px; margin:0; gap:3px}
  .mapmini.settled .segbar{opacity:.55}
  .mapmini .rtime b{font-size:11.5px; font-weight:600; color:var(--mut); font-variant-numeric:tabular-nums}
  .mapmini .rtime[hidden], .mapmini .ring[hidden]{display:none}
  /* #491: the chain's chapter strip along the widened map's bottom edge */
  .mapcards{position:absolute; left:10px; right:10px; bottom:10px; z-index:1; display:flex; gap:8px;
    overflow-x:auto; scrollbar-width:none; padding-bottom:2px}
  .mapcards::-webkit-scrollbar{display:none}
  .mapcards[hidden]{display:none}
  .mapcards .mcard{flex:0 0 auto; width:min(280px, 72%); padding:6px 10px; gap:9px; box-sizing:border-box;
    background:var(--card); border:1px solid var(--mapgrid); border-radius:8px; box-shadow:0 1px 4px rgba(31,41,51,.08)}
  .mapcards .mthumbs span{width:28px; height:28px}
  .mapcards .mname{font-size:12.5px; overflow:hidden; text-overflow:ellipsis}
  .mapcards .mname > span{overflow:hidden; text-overflow:ellipsis}
  .mapcards .mmeta{font-size:11px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis}
  .mapcards .mcount{font-size:11.5px}
  .mapbox{width:100%; background:var(--mapbg); border-radius:8px; display:block}
  .mapbox text{font-family:ui-monospace,Menlo,monospace}
  .stopdot{opacity:0; transition:opacity .5s}
  .stopdot.seen{opacity:1}
  /* Analyze scene (step 2): step 1's ticket + the moments as rows */
  /* the times row is reserved from the start (min-height): the stamps
     landing must not grow the ticket, and so the stage */
  .segtimes{display:flex; gap:4px; min-height:14px; font:10.5px/1.3 ui-monospace,SFMono-Regular,Menlo,monospace;
    color:#a5adb7; margin:2px 0 0}
  .segtimes span{flex:1; text-align:center; font-variant-numeric:tabular-nums}
  /* five rows show, the rest scroll (#318: the panel stops growing) —
     the same window as the photo list on upload: the same row (7px
     padding round two text lines beside a 34px thumb) and the same cap */
  .mrow{display:flex; flex-direction:column; max-height:261px; overflow-y:auto; scrollbar-width:thin;
    border-bottom:1px solid var(--line)}   /* the rule under the window, as on #upList */
  .mrow:not(:has(.mcard)), #upList:not(:has(.uprow)){border-bottom:0}   /* no rows, no rule */
  .mcard{display:flex; align-items:center; gap:12px; padding:7px 0; border-bottom:1px solid var(--line);
    font-size:13px; animation:pop .25s ease-out}
  .mcard:last-child{border-bottom:0}   /* the list's own rule closes it */
  .mthumbs{display:flex; gap:3px; flex:none}
  .mthumbs span{width:34px; height:34px; border-radius:5px; background:var(--line) center/cover no-repeat; display:block}
  .mthumbs span.drop{opacity:.35}
  .mtext{flex:1; min-width:0; display:flex; flex-direction:column}
  .mname{font-weight:600; font-size:13px; display:flex; align-items:center; gap:6px; white-space:nowrap;
    overflow:hidden; text-overflow:ellipsis}
  .mname .unk{color:#a5adb7; font-weight:500; font-style:italic}
  .mname svg{width:12px; height:12px; flex:none; color:var(--pin)}
  .mmeta{color:var(--mut); font-size:12px}
  .mcount{flex:none; color:var(--mut); font-size:12.5px; font-variant-numeric:tabular-nums; text-align:right}
  .mcount b{color:var(--ink); font-weight:600}
  /* Book step, cover scene: a diagram of the cover — the board colour,
     the countries map faded behind, five squares in an X, the title band */
  .mockcover{position:relative; width:100%; aspect-ratio:236/306; border-radius:6px; overflow:hidden;
    background:var(--board, #e8dfcf); box-shadow:0 10px 26px rgba(31,41,51,.18), inset 0 0 0 1px rgba(0,0,0,.05);
    transition:background .25s}
  .mockmap{position:absolute; left:0; top:0; width:100%; height:75.8%; opacity:.22; mix-blend-mode:multiply}
  .mockmap svg{width:100%; height:100%; display:block}
  .mockslots{position:absolute; inset:0}
  .mslot{position:absolute; background:#fff; box-shadow:0 2px 6px rgba(0,0,0,.18); padding:2.2%;
    box-sizing:border-box; cursor:pointer; border:0}
  .mslot i{display:block; width:100%; aspect-ratio:1; background:#eee9df center/cover no-repeat}
  .mslot b{position:absolute; left:6%; top:6%; font-size:9px; color:#a89f8d; font-weight:600}
  .mslot.sel{outline:2px solid var(--acc); outline-offset:2px}
  .mockband{position:absolute; left:0; right:0; bottom:0; height:24.2%; padding:6% 6% 0}
  .mocktitle{font:400 clamp(14px, 4.6cqw, 22px)/1.15 Georgia,"Times New Roman",serif; color:#1f2933; max-width:74%;
    overflow:hidden; text-overflow:ellipsis; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical}
  .mockcover{container-type:inline-size}
  .mockrule{width:34px; height:2px; background:#d97742; margin:9px 0 7px}
  .mockdate{font-size:8px; letter-spacing:.14em; color:#5a6272; text-transform:uppercase}
  .mockbrand{position:absolute; right:6%; bottom:9%; font-size:8px; letter-spacing:.18em; color:#1f2933}
  .mockbrand i{display:inline-block; width:7px; height:7px; border:1.5px solid #d97742; border-radius:50%; margin-right:5px; vertical-align:-1px}
  .mockcover.dark .mocktitle, .mockcover.dark .mockbrand, .mockcover.dark .mockdate{color:#f4f2ec}
  .swatches{display:flex; flex-wrap:wrap; gap:8px; align-items:center}
  .swatches .sw{width:26px; height:26px; padding:0; border-radius:50%; border:2px solid #fff;
    box-shadow:0 0 0 1px var(--line); cursor:pointer}
  .swatches .sw.on{box-shadow:0 0 0 2px var(--acc)}
  .swatches .sw:hover{box-shadow:0 0 0 2px var(--mut)}
  /* #223: the sheet under the prints, per cover style (data-kind on the
     mock): a taped paper sheet, a pinned linen one, none for a photo
     cover or the plain wall */
  .mocksheet{position:absolute; left:6%; top:5%; width:88%; height:66%; background:#f3ede2;
    box-shadow:0 2px 8px rgba(0,0,0,.16)}
  .mockcover[data-kind="taped"] .mocksheet{transform:rotate(1.2deg); box-shadow:2px 3px 8px rgba(43,39,36,.26)}
  .mockcover[data-kind="photo"] .mocksheet, .mockcover[data-kind="plain"] .mocksheet{display:none}
  .mockcover[data-kind="linen"] .mocksheet{background:#f6f1e6 repeating-linear-gradient(0deg,rgba(43,39,36,.05) 0 1px,transparent 1px 6px),
    repeating-linear-gradient(90deg,rgba(43,39,36,.05) 0 1px,transparent 1px 6px); transform:rotate(.4deg)}
  .mockcover[data-kind="sheet"] .mocksheet{transform:rotate(-.8deg)}
  .mockcover[data-kind="folded"] .mocksheet{transform:rotate(.5deg)}
  .fold{position:absolute; top:0; bottom:0; width:6%; transform:translateX(-50%);
    background:linear-gradient(90deg,rgba(43,39,36,0) 0%,rgba(43,39,36,.09) 46%,rgba(255,255,255,.55) 54%,rgba(255,255,255,0) 100%)}
  .fold.fh{left:0; right:0; width:auto; height:6%; top:50%; transform:translateY(-50%);
    background:linear-gradient(180deg,rgba(43,39,36,0) 0%,rgba(43,39,36,.08) 46%,rgba(255,255,255,.5) 54%,rgba(255,255,255,0) 100%)}
  .stwrap{position:relative}
  .stwrap .stile{width:100%}
  .sechead{display:flex; align-items:center; justify-content:space-between; gap:10px; margin-bottom:8px}
  .sechead h3{margin:0}
  .fchip.small{padding:3px 10px; font-size:11.5px}
  .fchip.small:hover{color:var(--acc); border-color:var(--acc)}
  /* the real tape (cover_artist.py): 22 × 7 mm beige strips with faint
     white edges, straddling the sheet's corners */
  .tape{position:absolute; width:11%; height:3.5%; background:rgba(233,220,190,.78);
    box-shadow:0 1px 2px rgba(43,39,36,.18); border-left:1px solid rgba(255,255,255,.35); border-right:1px solid rgba(255,255,255,.35)}
  .pinhead{position:absolute; width:5px; height:5px; border-radius:50%; background:#8a7f6e; box-shadow:0 1px 1px rgba(0,0,0,.3)}
  /* #223: the live cover over the diagram — a scaled frame of the real
     sheet's front (the viewer's cover-thumb technique) */
  .coverlive{position:absolute; inset:0; background:var(--mapbg)}
  .coverlive.tile{opacity:0; transition:opacity .2s}
  .coverlive.tile.ready{opacity:1}
  .cvlive{display:block; position:relative; width:100%; overflow:hidden}
  .cvmini{position:absolute; left:0; top:0; transform-origin:0 0; display:block}
  .cvmini iframe{position:absolute; border:0; pointer-events:none; background:#fff; display:block}
  .mslot.big{padding:2%}
  .mslot.big i{aspect-ratio:4/3}
  /* #223: the format row above the mock — one pressed chip per group */
  .format{display:flex; flex-wrap:wrap; gap:8px 14px; margin-bottom:14px; align-items:flex-end}
  .fgroup{display:flex; flex-direction:column; gap:4px}
  .flabel{font-size:10.5px; text-transform:uppercase; letter-spacing:.08em; color:var(--mut); font-weight:600}
  .format .fchip:disabled{cursor:default; opacity:1}
  .fchip .pr{font-size:10.5px; opacity:.7; margin-left:4px; font-weight:500}
  /* finish · paper · length, then the total */
  .product{display:flex; flex-wrap:wrap; gap:4px 0; margin-top:12px; font-size:12.5px; color:var(--mut); line-height:1.4}
  .product b{color:var(--ink); font-weight:600}
  .product .dot{padding:0 6px; color:var(--mut); opacity:.6}
  .pricebox{display:flex; align-items:baseline; gap:10px; flex-wrap:wrap; margin-top:12px; padding-top:12px; border-top:1px solid var(--line)}
  .pricebox .total{font-size:22px; font-weight:700; letter-spacing:-.01em; font-variant-numeric:tabular-nums}
  .pricebox .pricenote{font-size:12px; color:var(--mut)}
  /* #223: the cover style tiles — mini mocks of this book */
  .styles{display:grid; grid-template-columns:repeat(3, minmax(0, 1fr)); gap:10px}
  .stile{border:0; background:transparent; padding:0; text-align:center; font:inherit; font-size:11.5px; color:var(--mut); cursor:pointer}
  .stile .mini{display:block; box-shadow:0 3px 10px rgba(31,41,51,.16), inset 0 0 0 1px rgba(0,0,0,.05); border-radius:4px; margin-bottom:6px}
  .stile.on .mini{outline:2px solid var(--acc); outline-offset:3px}
  .stile.on{color:var(--seltext); font-weight:600}
  .mini .mockmap{opacity:.2}
  .mini .mockslots{display:block}
  .mini .mslot{display:block; padding:1.6%; box-shadow:0 1px 3px rgba(0,0,0,.2); cursor:inherit}
  .mini .mslot b{font-size:6px}
  .mini .pinhead{width:3px; height:3px}
  .mini .band{position:absolute; left:8%; right:8%; bottom:8%; height:14%}
  .mini .band i{display:block; height:3px; width:46%; background:#1f2933; opacity:.55; margin-bottom:3px}
  .mini .band i:last-child{width:14%; background:#d97742; opacity:1}
  .mini.dark .band i{background:#f4f2ec}
  .mini.dark .band i:last-child{background:#d97742}
  .sec .varpick{margin-top:12px}
  .sec .wallhead .spacer{flex:1}
  .wslot.sel{border-color:var(--acc); box-shadow:0 0 0 3px var(--accsoft)}
  .wslot.one{width:104px; height:78px}
  .wslot.empty{border-style:dashed; background:var(--bg); color:var(--mut); font-size:18px; font-weight:300}
  /* #223: the print picker — a dialog; a bottom sheet on a phone */
  /* block layout on purpose: a flex dialog with a capped height and a
     scrolling child collapses the child to nothing in Safari — the body
     gets its own scroll height instead (82vh minus head and foot) */
  .picker{border:0; border-radius:16px; padding:0; width:min(92vw, 640px);
    background:var(--card); color:var(--ink); box-shadow:0 24px 60px rgba(31,41,51,.28)}
  .picker::backdrop{background:rgba(31,41,51,.35)}
  .picker[open]{display:block}
  .pickhead{display:flex; align-items:flex-start; gap:12px; padding:18px 20px 12px; border-bottom:1px solid var(--line)}
  .pickhead > div{flex:1}
  .pickhead h3{margin:0; font-size:15px}
  .dlgx{width:30px; height:30px; border-radius:50%; border:0; background:transparent; color:var(--mut); font-size:15px; flex:0 0 auto}
  .dlgx:hover{background:var(--bg); color:var(--ink)}
  .pickbody{padding:0 20px 8px; overflow-y:auto; max-height:calc(82vh - 180px); -webkit-overflow-scrolling:touch}
  .pickbody .dayhead{margin:16px 0 8px}
  .pickbody .dayhead h3{font-size:14px}
  .pgrid{display:grid; grid-template-columns:repeat(6, minmax(0, 1fr)); gap:8px}
  .pcell{position:relative; aspect-ratio:1; border:2px solid transparent; border-radius:6px; padding:0; overflow:hidden; cursor:pointer;
    background:var(--bg) center/cover no-repeat}
  .pcell.cur{border-color:var(--acc); box-shadow:0 0 0 3px var(--accsoft)}
  .pcell .wn{position:absolute; top:4px; left:4px; width:17px; height:17px; border-radius:50%; background:var(--acc); color:#fff;
    font-size:10px; line-height:17px; text-align:center; font-weight:600}
  .pickfoot{display:flex; align-items:center; gap:10px; padding:12px 20px 16px; border-top:1px solid var(--line)}
  .pickfoot .spacer{flex:1}
  @media (max-width:760px){
    .picker{width:100vw; max-width:100vw; margin:auto 0 0; border-radius:16px 16px 0 0}
    .pickbody{max-height:calc(78vh - 180px)}
    .pgrid{grid-template-columns:repeat(4, minmax(0, 1fr))}
  }
  /* #223: each step's price */
  .pagerow button em{display:block; font-style:normal; font-size:10.5px; font-weight:500; color:var(--mut); margin-top:3px;
    font-variant-numeric:tabular-nums}
  .pagerow button[aria-checked="true"] em{color:var(--seltext)}
  /* the steps keep their width and the row scrolls — a long ladder never
     squashes; the chosen step is scrolled into view */
  .pagerow{display:flex; gap:6px; overflow-x:auto; scroll-snap-type:x proximity; scrollbar-width:thin;
    padding:2px 2px 6px; margin:0 -2px}
  .pagerow button{flex:0 0 76px; scroll-snap-align:center; text-align:center; padding:9px 0 7px; border:1px solid var(--line); border-radius:10px;
    background:var(--card); color:var(--ink); font-weight:600; font-variant-numeric:tabular-nums; font:inherit; cursor:pointer}
  .pagerow button small{display:block; font-weight:500; font-size:8.5px; color:var(--mut); letter-spacing:.05em;
    text-transform:uppercase; min-height:1.2em; white-space:nowrap; margin-top:2px}
  .pagerow button b{font-weight:600}
  .pagerow button[aria-checked="true"]{border-color:var(--acc); color:var(--seltext); background:var(--accsoft)}
  .pagerow button:disabled{opacity:.45; cursor:not-allowed}
  .pagerow button:disabled b{text-decoration:line-through; text-decoration-thickness:2px}
  .sec{margin-top:16px}
  .sec h3{margin:0 0 8px}
  /* layout scene: the real cover once composed, the mock until then.
     The grid row stretches and the chapters column is floored at the
     stage height (--stageh: what the upload column needs at rest — the
     same line as the map scenes), so the cover takes the row's height
     at its own aspect, centred, instead of the row following the cover */
  #sceneLayout .upgrid{align-items:stretch}
  #sceneLayout .upgrid > .panel:first-child{min-height:var(--stageh)}
  .coverpanel{display:flex; flex-direction:column}
  .coverpanel .realcover{flex:1 1 0; min-height:0; align-self:center; width:auto; max-width:100%}
  .coverpanel .hint{flex:none}
  .realcover{border-radius:6px; overflow:hidden; background:var(--mapbg); aspect-ratio:236/306;
    box-shadow:0 10px 26px rgba(31,41,51,.18)}
  .realcover iframe, .realcover .mockcover{width:100%; height:100%; border:0; display:block}
  .mapempty{font-size:13px; color:var(--mapmut); letter-spacing:.06em; text-transform:uppercase}

  /* ---------- step 2: review ---------- */
  /* #56: cover-prints tray — the wall picks, tap a slot then a photo */
  .wallbar{border:1px solid var(--line); background:var(--card);
    border-radius:12px; padding:10px 12px; margin:10px 0 14px}
  .wallhead{display:flex; align-items:center; gap:10px; flex-wrap:wrap}
  .walltitle{font-size:13px; font-weight:600}
  .wallsub{font-size:12px; color:var(--mut); flex:1}
  .wallslots{display:flex; gap:8px; margin-top:8px; align-items:flex-end}
  .wslot{position:relative; width:52px; height:52px; border-radius:8px;
    border:2px solid var(--line); padding:0; cursor:pointer;
    background-size:cover; background-position:center}
  .wslot:first-child{width:68px; height:68px}   /* the lead print is the big one */
  .wslot.sel{border-color:var(--acc); box-shadow:0 0 0 3px var(--accsoft)}
  .wslot .wn{position:absolute; top:-6px; left:-6px; width:16px; height:16px;
    border-radius:50%; background:var(--ink); color:var(--bg); font-size:10px;
    line-height:16px; text-align:center; font-weight:600}
  .cell .wbadge{position:absolute; right:5px; bottom:22px; z-index:2; width:17px;
    height:17px; border-radius:50%; background:var(--acc); color:#fff;
    font-size:10px; line-height:17px; text-align:center; font-weight:600}
  .cell .wbadge:empty{display:none}
  .filters{display:flex; gap:6px; margin:2px 0 18px}
  .fchip{border:1px solid var(--line); background:var(--card); color:var(--mut);
    border-radius:999px; padding:6px 14px; font-size:12.5px}
  .fchip[aria-pressed="true"]{background:var(--chipfill); color:var(--chipink); border-color:var(--chipline)}
  .fchip .fn{font-weight:600; opacity:.7; font-variant-numeric:tabular-nums}
  .fchip .fn:empty{display:none}
  /* #488: Generate as a split button — the primary and its caret share one
     shape; the menu opens above the bar, right-aligned */
  .splitbtn{position:relative; display:inline-flex; align-items:stretch}
  .splitbtn[hidden]{display:none}
  .splitbtn .btn{display:inline-flex; align-items:center}
  /* two lines inside the normal button height: the padding gives way to the sub-label */
  .splitbtn > #analyzeBtn{flex-direction:column; align-items:flex-start; justify-content:center; gap:0; line-height:1.1}
  .splitbtn > #analyzeBtn:has(> .sub:not([hidden])){padding-block:4px; min-height:43px; box-sizing:border-box}
  .splitbtn > #analyzeBtn .sub{font-size:10.5px; font-weight:500; color:inherit; opacity:.95; letter-spacing:.02em; line-height:1.1; margin:0}
  .splitbtn > #analyzeBtn .sub[hidden]{display:none}
  .splitbtn:has(> .caret:not([hidden])) > #analyzeBtn{border-radius:10px 0 0 10px; padding-right:14px}
  .splitbtn > .caret{border-radius:0 10px 10px 0; padding:0 11px; font-size:12px; border-left:1px solid rgba(255,255,255,.3)}
  .splitbtn > .caret[hidden]{display:none}
  .splitbtn > .caret[aria-expanded="true"]{filter:brightness(1.12)}
  .splitbtn > #analyzeBtn.ghost{border-right:0}
  .splitbtn:has(> #analyzeBtn.ghost) > .caret{background:var(--card); color:var(--acc); border:1px solid var(--acc); border-left:0}
  .genmenu{position:absolute; right:0; bottom:calc(100% + 8px); z-index:20; min-width:250px; padding:6px;
    background:var(--card); border:1px solid var(--line); border-radius:10px; box-shadow:0 10px 28px rgba(31,41,51,.16)}
  .genmenu[hidden]{display:none}
  .genmenu button{display:flex; flex-direction:column; align-items:flex-start; gap:2px; width:100%; text-align:left;
    padding:8px 30px 8px 10px; border:0; border-radius:7px; background:transparent; color:var(--ink); font:inherit; cursor:pointer; position:relative}
  .genmenu button:hover, .genmenu button:focus-visible{background:var(--accsoft); outline:0}
  .genmenu button b{font-size:13.5px; font-weight:600}
  .genmenu button small{font-size:11.5px; color:var(--mut)}
  .genmenu button[aria-checked="true"]::after{content:"✓"; position:absolute; right:10px; top:8px; color:var(--acc); font-weight:700}
  /* #488: while the chain runs, the ticket bars offer one thing — Cancel;
     the moves the chain makes itself (Review, See my book, Back…) hide */
  [data-gen-cancel]{display:none}
  body.chain #sceneAnalyze .stickybar .btn:not([data-gen-cancel]),
  body.chain #sceneWords .stickybar .btn:not([data-gen-cancel]),
  body.chain #sceneLayout .stickybar .btn:not([data-gen-cancel]){display:none}
  body.chain .btn[data-gen-cancel]{display:inline-flex}
  .fempty{color:var(--mut); font-size:13.5px; margin:0 0 18px}
  /* a filter with nothing to show hides its day sections rather than
     leaving bare headers behind (#12) */
  .review.f-kept .daysec:not(:has(.cell:not(.off))){display:none}
  .review.f-dropped .daysec:not(:has(.cell.off, .cell.dup)){display:none}
  /* the chapter head: title and count on one line, the places + date
     subtitle on its own line beneath */
  .dayhead{display:flex; align-items:baseline; gap:2px 10px; margin:22px 0 10px; flex-wrap:wrap}
  .dayhead h3{margin:0; font-size:15px}
  .dayhead .loc{color:var(--mut); font-size:12.5px; flex-basis:100%; order:3}
  .dayhead .loc .blurb{display:block; margin-top:2px; color:var(--ink); font-style:italic}
  #days .summary{margin:6px 0 4px; max-width:64ch; font-style:italic; color:var(--ink); line-height:1.5}
  .dayhead .heritage{font-size:11px; font-weight:600; letter-spacing:.06em; text-transform:uppercase; color:var(--terra); white-space:nowrap}
  .dayhead .count{margin-left:auto; font-size:12px; color:var(--mut); font-variant-numeric:tabular-nums; order:2}
  /* the chevron folds the chapter to its head; Keep all / Drop all act on
     the whole chapter and hide where nothing in it can toggle */
  .dayhead .fold{width:18px; height:18px; border:0; padding:0; background:none; cursor:pointer;
    color:var(--mut); align-self:center; position:relative}
  .dayhead .fold::before{content:""; position:absolute; left:5px; top:3px; width:7px; height:7px;
    border-right:1.5px solid currentColor; border-bottom:1.5px solid currentColor;
    transform:rotate(45deg); transition:transform .15s}
  .daysec.closed .fold::before{transform:rotate(-45deg); top:5px}
  .daysec.closed .grid{display:none}
  .dayhead .bulk{display:inline-flex; gap:4px; order:1; margin-left:6px}
  .dayhead .bulk button{border:1px solid var(--line); background:var(--card); color:var(--mut);
    border-radius:999px; padding:2px 9px; font-size:11.5px; cursor:pointer}
  .dayhead .bulk button:hover{color:var(--ink); border-color:var(--chipline)}
  .daysec:not(:has(.cell:not(.lock):not(.dup))) .bulk{display:none}
  .daysec:not(:has(.cell.off:not(.lock):not(.dup))) .bulk [data-keep="1"]{display:none}
  .daysec:not(:has(.cell:not(.off):not(.lock):not(.dup))) .bulk [data-keep="0"]{display:none}
  .daysec.busy .bulk button{opacity:.5; pointer-events:none}
  .grid{display:grid; grid-template-columns:repeat(6,1fr); gap:8px}
  @media (max-width:760px){ .grid{grid-template-columns:repeat(3,1fr)} }
  .cell{aspect-ratio:1; cursor:pointer; border:0; padding:0}
  .cell, .thumbsq{background-size:cover; background-position:center}
  .cell.dup{display:none}
  .review.f-dropped .cell.dup{display:block}
  .cell .tick{position:absolute; top:6px; left:6px; width:18px; height:18px; border-radius:50%;
    background:var(--acc); color:#fff; z-index:2; font-size:12px; line-height:18px; text-align:center}
  .cell.off{opacity:.35; filter:saturate(.4)}
  .cell .why{opacity:.85}
  .cell:not(.off) .why{display:none}  /* re-selected by hand: the verdict no longer applies */
  .cell.off .tick{background:rgba(255,255,255,.85); color:transparent; border:1.5px solid #9aa5b1}
  .cell .upin{position:absolute; top:5px; left:28px; z-index:2; font-size:11px;
    filter:drop-shadow(0 1px 1px rgba(0,0,0,.6))}
  .cell .qflag{position:absolute; top:5px; right:28px; z-index:2; font-size:11px;
    filter:drop-shadow(0 1px 1px rgba(0,0,0,.6))}
  .cell .geo{position:absolute; top:6px; right:6px; z-index:2; width:17px; height:17px;
    border-radius:50%; background:rgba(15,25,35,.55); padding:2.5px; color:#fff}
  .cell .geo.none{color:#c3cbd4; opacity:.85}
  .cell.off .geo{opacity:.5}
  .cell .place{position:absolute; left:0; right:0; bottom:0; z-index:2; font-size:10px; text-align:left;
    color:#fff; background:linear-gradient(transparent,rgba(0,0,0,.55));
    padding:14px 6px 4px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis}
  .review.f-kept .cell.off{display:none}
  .review.f-dropped .cell:not(.off){display:none}

  .stickybar{position:fixed; left:0; right:0; bottom:0; z-index:15; display:flex; align-items:center;
    gap:14px; background:var(--card); border-top:1px solid var(--line);
    padding:13px 26px calc(13px + env(safe-area-inset-bottom)); font-size:13.5px; flex-wrap:wrap}
  .stickybar .n{font-weight:700; font-variant-numeric:tabular-nums}
  .stickybar .spacer{flex:1}
  .stickybar .saved{color:var(--mut); font-size:12.5px}
  /* phones: drop the prose, keep the bar to a status line + a button row */
  /* phones, either way round (narrow, or short = landscape): ONE row —
     the primary grows, a ghost shrinks to its glyph (data-glyph; the
     label stays the accessible name), the status text rides along muted
     and ellipsised, prose (two-line hints) is dropped */
  @media (max-width:560px), (max-height:500px){
    .stickybar{flex-wrap:nowrap; gap:10px; padding:10px 14px calc(10px + env(safe-area-inset-bottom))}
    .stickybar .saved, .stickybar .spacer{display:none}
    .stickybar > span:first-child{flex:0 1 auto; min-width:0; color:var(--mut); font-size:12px;
      white-space:nowrap; overflow:hidden; text-overflow:ellipsis}
    .stickybar > span:first-child.prose{display:none}
    .stickybar .btn{flex:1; padding:11px 14px; text-align:center; white-space:nowrap}
    .stickybar .btn.ghost[data-glyph]{flex:none; width:42px; height:42px; padding:0;
      border-radius:50%; font-size:0; display:grid; place-items:center}
    .stickybar .btn.ghost[data-glyph]::before{content:attr(data-glyph); font-size:18px; line-height:1}
  }

  /* ---------- interstitial ---------- */
  .overlay{position:fixed; inset:0; z-index:40; background:rgba(251,250,248,.94);
    display:none; place-items:center; padding:24px}
  .overlay.on{display:grid}
  .ovcard{background:var(--card); border:1px solid var(--line); border-radius:16px;
    padding:30px 34px; max-width:420px; width:100%; text-align:center;
    box-shadow:0 24px 60px -30px rgba(31,41,51,.35)}
  .ovcard h3{margin:0 0 6px; font-size:17px}
  .ovcard p{color:var(--mut); font-size:13px; margin:0 0 18px}
  .ovprog{height:6px; border-radius:3px; background:var(--line); overflow:hidden}
  .ovprog i{display:block; height:100%; width:0; background:var(--live); transition:width .12s linear}
  .ovline{font-size:12.5px; color:var(--mut); margin-top:12px; min-height:18px;
    font-variant-numeric:tabular-nums}

  /* ---------- step 3: preview ---------- */
  .titlerow{display:flex; align-items:baseline; gap:10px; flex-wrap:wrap}
  .booktitle{font-size:22px; font-weight:700; letter-spacing:-.01em; border-radius:6px; padding:0 4px;
    display:inline-block; max-width:100%; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; vertical-align:bottom}
  .booktitle:focus{overflow-x:auto; text-overflow:clip}
  .booktitle[contenteditable]:hover{background:var(--accsoft)}
  .editpen{font-size:11.5px; color:var(--mut)}
  /* quiet right-aligned download icon; retires when ordering goes live */
  .book{display:grid; grid-template-columns:1fr 1fr; background:var(--card);
    border:1px solid var(--line); border-radius:6px; overflow:hidden;
    box-shadow:0 24px 44px -30px rgba(31,41,51,.4); max-width:680px; margin:18px auto 0}
  /* the book desk while the very first design composes: the work ticket
     stands where the viewer will (bookMsg) */
  .book.bind{display:block; padding:30px; text-align:left}
  .book.bind .ticket.inline{margin:0}
  .bindwho{font-size:13px; color:var(--mut); min-height:1.5em; text-align:center}
  .bindwho b{color:var(--ink)}
  /* the BookViewer host (book-viewer.js supplies its own chrome) */
  .book.viewer{display:block; max-width:1100px; background:none; border:0;
    border-radius:0; overflow:visible; box-shadow:none; padding:0;
    position:relative}   /* anchors the redesign veil (#9) */
  /* redesign veil (#9): the previous book stays visible, dimmed, while
     the Art Director re-paces — indeterminate by design (one LLM call) */
  .rebindveil{position:absolute; inset:0; z-index:6; display:flex;
    align-items:center; justify-content:center; border-radius:12px;
    background:rgba(246,244,241,.72)}
  .rebindcard{display:flex; align-items:center; gap:10px; font-size:13px;
    color:var(--ink); background:var(--card,#fff); padding:10px 16px;
    border:1px solid var(--mapgrid); border-radius:999px;
    box-shadow:0 6px 18px rgba(31,41,51,.14)}
  .rebindcard b{color:var(--ink)}
  .rebindspin{width:15px; height:15px; flex:none; border-radius:50%;
    border:2px solid var(--mapgrid); border-top-color:var(--live);
    animation:rebindspin .8s linear infinite}
  @keyframes rebindspin{to{transform:rotate(360deg)}}
  /* v2 preview: a fixed two-page frame — cover alone right, facing
     pairs, the closing page alone left — laid open on the desk */
  .book.v2{display:block; background:linear-gradient(180deg,#efece5,#e7e3d9);
    border-color:var(--mapgrid); border-radius:14px; padding:26px 22px; overflow:visible;
    box-shadow:none}
  .sheetwrap{position:relative; overflow:hidden; width:100%}
  .book.v2 .sheetwrap{overflow:visible}
  /* pgstage/pgclip/pg-void sizes are set inline from the plan's page
     geometry (portrait, square, …) — see bookGeo() in app.js */
  .pgstage{display:flex; transform-origin:0 0}
  /* drop-shadow follows the pages' alpha, so a bookend view (transparent
     void beside the cover) casts a shadow under the page alone */
  .book.v2 .pgstage{filter:drop-shadow(0 12px 22px rgba(31,41,51,.22))}
  .pgclip{overflow:hidden; flex:none}
  .pgclip + .pgclip{border-left:1px solid rgba(31,41,51,.15)}
  .pg-void{flex:none; background:rgba(31,41,51,.05)}
  .book.v2 .pg-void{background:transparent}
  .sheet-frame{display:block; border:0; background:#fff}
  .page{aspect-ratio:1; position:relative; padding:20px; overflow:hidden}
  .page + .page{border-left:1px solid var(--line)}
  .page.dark{background:var(--mapbg); color:var(--maproute)}
  .page.cloth{background:linear-gradient(150deg,#17384a,#0e2534); color:#e8eef2;
    display:grid; place-items:center; text-align:center}
  .pfull{position:absolute; inset:0; border-radius:0}
  .pcap{position:absolute; bottom:12px; left:20px; right:20px; z-index:3; font-size:11px;
    color:#fff; text-shadow:0 1px 4px rgba(0,0,0,.65); line-height:1.45}
  .pcap b{color:#fff}
  .pcap.onpaper{position:static; color:var(--mut); text-shadow:none; margin-top:10px}
  .pcap.onpaper b{color:var(--ink)}
  .cap[contenteditable]{border-radius:4px; padding:1px 3px; display:inline}
  .cap[contenteditable]:hover{background:rgba(255,255,255,.18); outline:1px dashed rgba(255,255,255,.6)}
  .pcap.onpaper .cap[contenteditable]:hover{background:var(--accsoft); outline:1px dashed var(--acc)}
  .cap:focus{background:#fff; color:var(--ink); text-shadow:none; outline:2px solid var(--acc)}
  .duo{display:grid; grid-template-rows:1fr auto 1fr auto; gap:6px; height:100%}
  .duo .photo{width:100%; height:100%}
  .duo .dcap{font-size:10px; color:var(--mut); line-height:1.35}
  .pgnav{display:flex; align-items:center; justify-content:center; gap:16px; margin:16px 0 6px;
    color:var(--mut); font-size:13px}
  .pgbtn{border:1px solid var(--line); background:var(--card); border-radius:8px;
    padding:10px 16px; color:var(--ink)}
  .pgbtn:disabled{opacity:.4; cursor:default}
  /* #56 swap strip: every kept photo, the current prints badged */
  .wallpickstrip{display:flex; gap:6px; margin-top:10px; overflow-x:auto; padding-bottom:4px;
    scrollbar-width:thin}
  .wpick{position:relative; flex:0 0 52px; height:52px; border-radius:8px; padding:0;
    border:2px solid var(--line); background-size:cover; background-position:center; cursor:pointer}
  .wpick.on{border-color:var(--acc)}
  .wpick .wn{position:absolute; top:-6px; left:-6px; width:16px; height:16px; border-radius:50%;
    background:var(--acc); color:#fff; font-size:10px; line-height:16px; text-align:center; font-weight:600}
  /* the template's user knobs (cover colour) on the Book step */
  .varpick{display:flex; flex-direction:column; gap:8px}
  .varpick .varrow{display:flex; gap:6px; align-items:center; flex-wrap:wrap}
  .fchip .sw{display:inline-block; width:12px; height:12px; border-radius:50%;
    border:1px solid rgba(0,0,0,.18); vertical-align:-1px; margin-right:6px}
  /* a stored colour the admin switched off (#396): kept, marked */
  .varpick .fchip.off{outline:1.5px dashed var(--mut); outline-offset:2px; opacity:.75}
  .datelabel{font-size:12.5px; color:var(--mut); margin-right:4px}
  .lenpick{display:flex; flex-direction:column; gap:4px; color:var(--mut);
    font-size:11.5px; letter-spacing:.03em; text-transform:uppercase}
  .lenpick{width:min(100%, 420px); --cover:#2f4858; --cover2:#8c6a4f}
  /* the length ladder on Review: one card under the day list */
  .lenrow{border:1px solid var(--line); background:var(--card); border-radius:12px;
    padding:12px 14px 10px; margin:22px 0 0}
  .lenrow .lenpick{width:100%}
  .lenrow .covers{max-width:520px}
  /* the covers wheel (#15): every step is a small book; the centre one is
     the pick, impossible steps are blank boards, a padded step shows its
     blank pages as a paper band. Swipe / drag / scroll / ‹ › / arrows. */
  .covers{position:relative; height:128px; border-radius:12px; background:var(--mapbg); overflow:hidden}
  .covers::after{content:""; position:absolute; inset:0; pointer-events:none; z-index:2;
    background:linear-gradient(90deg, var(--mapbg) 0, transparent 16%, transparent 84%, var(--mapbg) 100%)}
  .cvscroll{position:absolute; inset:0; overflow-x:auto; overflow-y:hidden; scroll-snap-type:x mandatory;
    scrollbar-width:none; display:flex; align-items:center; cursor:grab}
  .cvscroll::-webkit-scrollbar{display:none}
  .cvscroll.drag{cursor:grabbing; scroll-snap-type:none}
  .cvpad{flex:0 0 calc(50% - 44px)}
  .cvitem{flex:0 0 88px; height:128px; scroll-snap-align:center; display:flex; flex-direction:column;
    align-items:center; justify-content:center; gap:5px; user-select:none; color:var(--mut);
    text-transform:none; letter-spacing:0; will-change:transform,opacity}
  .cvitem .cv{width:54px; height:70px; border-radius:3px 6px 6px 3px; position:relative; overflow:hidden;
    background:linear-gradient(100deg, var(--cover) 0 6%, var(--cover2) 6% 100%);
    box-shadow:0 3px 8px rgba(31,41,51,.14); display:flex; align-items:flex-end; justify-content:center;
    padding-bottom:6px; transition:box-shadow .15s}
  .cvitem .cv b{color:#fff; font-size:15px; font-weight:600; font-variant-numeric:tabular-nums;
    text-shadow:0 1px 2px rgba(0,0,0,.35)}
  .cvitem .cv i{position:absolute; left:0; right:0; top:0;
    background:repeating-linear-gradient(0deg, var(--mapbg) 0 2px, var(--mapgrid) 2px 3px)}
  .cvitem small{font-size:9.5px; letter-spacing:.06em; text-transform:uppercase; white-space:nowrap; min-height:1.2em}
  .cvitem.rec small{color:var(--good); font-weight:600}
  .cvitem.on .cv{box-shadow:0 0 0 2px var(--acc), 0 8px 14px rgba(31,41,51,.18)}
  .cvitem.off{opacity:.3 !important; cursor:not-allowed}
  .cvitem.off .cv{background:var(--mapgrid); box-shadow:none}
  .cvitem.off .cv b{color:var(--mut); text-shadow:none; text-decoration:line-through; text-decoration-thickness:2px}
  .cvbar{display:flex; align-items:center; gap:10px; margin-top:6px}
  .cvstep{border:1px solid var(--line); background:var(--card); color:var(--ink); border-radius:8px;
    width:32px; height:28px; font-size:13px; padding:0}
  .cvstep:disabled{opacity:.4; cursor:default}
  .cvval{font-size:12.5px; color:var(--mut); text-transform:none; letter-spacing:0}
  .cvval b{color:var(--ink); font-size:14px; font-variant-numeric:tabular-nums}
  @media (prefers-reduced-motion: reduce){ .cvitem{transform:none !important} }
  .lennote{font-size:11.5px; color:var(--mut); text-transform:none; letter-spacing:0;
    line-height:1.4; min-height:1.4em}
  .lennote b{color:var(--ink)}
  .dots{display:flex; gap:6px; justify-content:center; margin-top:4px}
  .dot{width:7px; height:7px; border-radius:50%; background:var(--line); border:0; padding:0;
    cursor:pointer}
  .dot.cap{border-radius:2px}   /* the cover and back-cover bookends read as boards */
  .dot[aria-current="true"]{background:var(--acc)}
  .editnote{text-align:center; color:var(--mut); font-size:12.5px; margin-top:14px}
  .editnote b{color:var(--ink)}

  /* #107: the book is frozen while an order is open */
  .lockbar{display:flex; gap:10px; align-items:center; flex-wrap:wrap; margin:0 0 14px;
    padding:10px 14px; border-radius:12px; background:var(--accsoft); color:var(--acc); font-size:13px}
  .lockbar b{font-weight:700}
  .lockbar .spacer{flex:1}
  .lockbar .btn.small{padding:6px 12px; font-size:12.5px}
  .locked .cell{pointer-events:none}
  .locked #bookTitle{pointer-events:none}
  .locked .editpen{display:none}
  .locked #modeRow, .locked #wallBar{pointer-events:none; opacity:.6}

  /* ---------- step 4: order (#104) ---------- */
  .summary{max-width:640px; margin:0 auto}
  .sumrow{display:flex; justify-content:space-between; padding:8px 0; font-size:13.5px;
    border-bottom:1px solid var(--line); font-variant-numeric:tabular-nums}
  .sumrow:last-child{border-bottom:0}
  .sumrow .l{color:var(--mut)}
  /* #304: the discount code, under the receipt */
  .codepanel{padding:10px 14px}
  .coderow{display:flex; align-items:center; gap:10px; flex-wrap:wrap}
  .codelbl{font-size:11.5px; color:var(--mut); text-transform:uppercase; letter-spacing:.06em}
  .codein{display:flex; align-items:center; gap:8px; flex:1; min-width:180px}
  .codein input{flex:1; min-width:0; text-transform:uppercase; font-variant-numeric:tabular-nums; letter-spacing:.04em}
  .codemsg{font-size:12.5px; margin-top:6px; min-height:0}
  .codemsg:empty{display:none}
  .codemsg.ok{color:var(--good)}
  .codemsg.no{color:var(--warn)}
  .addr{max-width:640px; margin:14px auto 0}
  .addrgrid{display:grid; grid-template-columns:1fr 1fr; gap:10px 12px}
  @media (max-width:520px){ .addrgrid{grid-template-columns:1fr} .addrgrid .wide{grid-column:auto} }
  .addrgrid label{display:flex; flex-direction:column; gap:4px; font-size:11.5px; color:var(--mut);
    text-transform:uppercase; letter-spacing:.06em}
  .addrgrid .wide{grid-column:1 / -1}
  .addrgrid input, .addrgrid select{font:inherit; font-size:14px; color:var(--ink); padding:9px 11px;
    border:1px solid var(--line); border-radius:9px; background:var(--card); text-transform:none;
    letter-spacing:0}
  .addrgrid input:focus, .addrgrid select:focus{outline:none; border-color:var(--acc);
    box-shadow:0 0 0 3px var(--accsoft)}
  .addrgrid input.bad{border-color:var(--warn); box-shadow:0 0 0 3px var(--warnsoft)}
  .addrnote{margin:10px 0 0; font-size:12px; color:var(--mut)}
  /* #303: the receipt rows inside the summary panel */
  .sumrow.promo span:last-child{color:var(--good)}
  .sumrow.total{border-top:1px solid var(--line); margin-top:4px; padding-top:10px; font-size:15px}
  .sumrow.total .l{color:var(--ink)}
  .sumrow.total small{font-weight:400; color:var(--mut); font-size:12px; margin-left:6px}
  .sumrow .mut{color:var(--mut)}
  .rcpt.err{padding:8px 0; font-size:13px; color:var(--danger)}
  /* the delivery chooser (#310): Gelato's methods for the address */
  .linkbtn{background:none; border:0; padding:0 0 0 6px; font:inherit; font-size:12px; font-weight:600; color:var(--acc); cursor:pointer}
  .linkbtn:hover{text-decoration:underline}
  .shipopts{padding:8px 0 4px; border-bottom:1px solid var(--line)}
  .shiphead{font-size:11.5px; color:var(--mut); margin-bottom:6px}
  .shipopt{display:flex; align-items:center; gap:10px; padding:7px 10px; margin:0 0 6px;
    border:1px solid var(--line); border-radius:10px; font-size:13.5px; cursor:pointer; background:var(--card)}
  .shipopt.on{border-color:var(--acc); background:var(--accsoft)}
  .shipopt input{margin:0; accent-color:var(--acc)}
  .shipopt .shipname{flex:1; display:flex; flex-direction:column; gap:1px}
  .shipopt .shipname em{font-style:normal; font-size:11px; font-weight:700; color:var(--good); margin-left:6px}
  .shipopt .shipname small{font-size:11.5px; color:var(--mut)}
  .shipopt .shipprice{font-variant-numeric:tabular-nums}
  /* ---------- step 4, the canvas build (#311) ---------- */
  .btn.small{padding:5px 12px; font-size:12.5px; border-radius:999px}
  a.btn{text-decoration:none}
  .ordgrid{display:grid; grid-template-columns:1.12fr .88fr; gap:18px; align-items:start; max-width:900px; margin:0 auto}  /* #318: the same split as .upgrid */
  @media (max-width:760px){ .ordgrid{grid-template-columns:1fr} }
  .ordcol{display:flex; flex-direction:column; gap:18px}
  .ordcol .panel.empty{display:none}       /* toggled by renderReceipt (Safari skips :has invalidation) */
  .bookcard{display:flex; gap:18px; align-items:center}
  .bookmeta{display:flex; flex-direction:column; gap:4px; min-width:0}
  .bookmeta .bt{font-size:15px; font-weight:650}
  .bookmeta .bm{font-size:13px; color:var(--mut)}
  .bookmeta .bm:empty{display:none}
  .bookmeta .linkbtn{padding:0; margin-top:4px; align-self:flex-start; font-size:12.5px}
  .ordcover{border:0; padding:0; background:none; cursor:pointer; display:block; flex-shrink:0}
  .mbthumb.big{width:96px; border-radius:6px; box-shadow:0 6px 18px rgba(31,41,51,.22)}
  .mbthumb.big b{font:700 11px/1.25 "Segoe UI",system-ui,sans-serif; padding:6px 8px 8px}
  .ordcover:hover .mbthumb{box-shadow:0 8px 26px rgba(31,41,51,.32)}
  .securenote{display:flex; gap:10px; align-items:flex-start; font-size:12.5px; color:var(--mut); padding:0 4px}
  .securenote svg{flex-shrink:0; margin-top:2px; color:var(--good)}
  .addr h3.sub{margin-top:14px}
  .addrgrid label small{font-weight:400; text-transform:none; letter-spacing:0; color:var(--mapmut); margin-left:4px}
  .ordbanner{display:flex; gap:12px; align-items:center; flex-wrap:wrap; max-width:900px; margin:0 auto 14px;
    padding:10px 14px; border-radius:10px; font-size:13px}
  .ordbanner.acc{background:var(--goodsoft); color:var(--good)}
  .ordbanner.warn{background:var(--warnsoft); color:var(--warn)}
  .ordbanner .spacer{flex:1}
  .ordbanner .btn.ghost{color:inherit; border-color:currentColor; background:transparent}
  #ordCard{max-width:900px; margin:0 auto 18px; display:flex; flex-direction:column; gap:18px}
  .ordslim{display:flex; align-items:baseline; gap:12px; flex-wrap:wrap; font-size:13.5px; padding:12px 18px}
  .ordslim b{color:var(--mut)}
  .ordslim .oid{margin-left:auto; font-family:ui-monospace, SFMono-Regular, Menlo, monospace; font-size:12px; color:var(--mut)}
  #ordCard[hidden]{display:none}
  .ordmain{display:grid; grid-template-columns:auto 1fr; gap:22px; align-items:center; padding:22px}
  .ordmain .rail{grid-column:1 / -1}
  .ordstat{display:flex; flex-direction:column; gap:8px; min-width:0}
  .ordstat .ordh{font-size:18px; font-weight:650}
  .ordstat .ordh.acc{color:var(--good)}
  .ordstat .ordh.warn{color:var(--warn)}
  .ordstat p{margin:0; font-size:13.5px}
  .ordstat .clock{color:var(--warn); font-variant-numeric:tabular-nums}
  .ordacts{display:flex; gap:10px; align-items:center; flex-wrap:wrap; margin-top:6px}
  .ordacts code{font-size:12.5px; color:var(--mut)}
  .rail{display:grid; grid-template-columns:repeat(5, minmax(0, 1fr)); margin-top:14px}
  .rs{display:flex; flex-direction:column; align-items:center; gap:6px}
  .rline{display:flex; align-items:center; width:100%}
  .rline i{flex:1; height:2px; background:var(--line)}
  .rs:first-child .rline i.a, .rs:last-child .rline i.z{visibility:hidden}
  .rs.done .rline i, .rs.cur .rline i.a{background:var(--live)}
  .rline b{width:18px; height:18px; border-radius:50%; border:2px solid var(--line); background:var(--card);
    display:flex; align-items:center; justify-content:center; flex-shrink:0; box-sizing:border-box}
  .rs.done .rline b{background:var(--live); border-color:var(--live)}
  .rs.cur .rline b{border-color:var(--live)}
  .rline b svg{width:10px; height:10px; display:block}
  .rline u{width:6px; height:6px; border-radius:50%; background:var(--live); display:block}
  .rs .rn{font-size:12.5px; color:var(--mut); text-align:center}
  .rs.done .rn, .rs.cur .rn{color:var(--ink)}
  .rs.cur .rn{font-weight:650}
  .rs .rsub{font-size:11px; color:var(--mapmut); text-align:center; margin-top:-4px}
  .ordcols{display:grid; grid-template-columns:1fr 1fr; gap:18px; align-items:start}
  @media (max-width:600px){ .ordcols{grid-template-columns:1fr} .ordmain{grid-template-columns:1fr; justify-items:center; text-align:center}
    .ordstat{align-items:center} .rs .rsub{display:none} }
  .ordpanel{display:flex; flex-direction:column; gap:10px}
  .ordpanel .ph{display:flex; align-items:baseline; justify-content:space-between; gap:10px; font-size:13px}
  .ordpanel .addr2{font-size:13px; color:var(--mut); line-height:1.55; white-space:pre-line}
  .ordfoot{display:flex; align-items:center; gap:14px; font-size:12px; color:var(--mut); padding:0 4px; flex-wrap:wrap}
  /* #513: the share card panel */
  .sharepanel{display:flex; flex-direction:row; gap:18px; align-items:center; margin:12px 0}
  .sharepanel .sharecard{width:132px; aspect-ratio:4/5; object-fit:cover; border-radius:4px; box-shadow:0 6px 18px rgba(31,42,68,.18); flex:0 0 auto; background:var(--line)}
  .sharepanel .sharetxt{min-width:0; display:flex; flex-direction:column; gap:4px}
  .sharepanel .sharetxt b{font-size:16px}
  .sharepanel .sharetxt p{margin:0; color:var(--mut); font-size:14px}
  @media (max-width:560px){ .sharepanel{flex-direction:column; align-items:flex-start} .sharepanel .sharecard{width:160px} }
  .ordfoot .spacer{flex:1}
  .ordfoot .oid{font-family:ui-monospace, SFMono-Regular, Menlo, monospace}
  .linkbtn.mut{color:var(--mut); font-size:12.5px; font-weight:500; padding:0}
  .linkbtn.mut:hover{color:var(--warn)}
  .ordrcpt{max-width:360px; margin:8px auto 10px; text-align:left}
  .pricebox .was{font-size:14px; color:var(--mut); text-decoration:line-through; font-weight:400; margin-right:6px}
  .ordererr{color:var(--warn); font-size:12.5px}
  .ordererr:empty{display:none}
  .success{max-width:640px; margin:14px auto 0; text-align:center;
    background:var(--accsoft); border-radius:14px; padding:28px}
  .success h3{margin:0 0 6px; color:var(--good); font-size:18px}
  .success p{color:var(--ink); font-size:13.5px; margin:0 0 6px}
  .success .oid{font-family:ui-monospace, SFMono-Regular, Menlo, monospace; font-size:12px; color:var(--mut)}
  .success .addr2{font-size:12.5px; color:var(--mut); margin-top:10px; white-space:pre-line}
  .success a{color:var(--acc)}
  /* #25: the ordered book's cover on its status card — a button into the
     read-only viewer; two columns on desktop, stacked on a phone */
  .success.hascover{display:flex; gap:22px; align-items:center; text-align:left; padding:22px}
  .success .ordcover{flex-shrink:0; border:0; padding:0; background:none; cursor:pointer; display:block}
  .success .ordcover .mbthumb{border-radius:6px; box-shadow:0 6px 18px rgba(31,41,51,.22)}
  .success .ordcover:hover .mbthumb{box-shadow:0 8px 26px rgba(31,41,51,.32)}
  .success .ordcover .mbthumb{width:96px; margin:0}
  .success .ordcover .mbthumb.realcover{width:auto}
  .success .ordcover small{display:block; text-align:center; font-size:11.5px; color:var(--mut); margin-top:6px}
  .success .ordbody{flex:1; min-width:0}
  @media (max-width:560px){ .success.hascover{flex-direction:column; text-align:center} }
  /* the read-only viewer overlay for an ordered book */
  #bookPeek{position:fixed; inset:0; z-index:900; background:var(--bg,#fff);
    display:flex; flex-direction:column}
  #bookPeek .peekbar{display:flex; align-items:center; gap:10px; padding:10px 14px;
    border-bottom:1px solid var(--line); font-weight:600}
  #bookPeek #peekHost{flex:1; min-height:0; display:flex; flex-direction:column}
  #bookPeek .bv{flex:1; min-height:0; border:0; border-radius:0}
  #bookPeek .bv-stage{height:auto; flex:1; min-height:0}
  .b-order{background:var(--infosoft); color:var(--info)}

  /* BOOK-MODES: verdict + mode switch on the review screen */
  .reviewhead{display:flex; align-items:center; justify-content:space-between; gap:12px 18px; flex-wrap:wrap;
    margin-bottom:14px}   /* the map strip follows the heading directly */
  .reviewhead h2{margin:0}
  .moderow{display:flex; align-items:center; flex-wrap:wrap; gap:8px; justify-content:flex-end}
  .moderow:empty{display:none}
  .modestale{font-size:12px; color:var(--warn)}
