/* ============ Lotwize design system (2026-07 redesign) ============ */
:root{
  /* exact Figma variables (canvas "Final"): H colors / Paragraph / Support text / #357CFF */
  --ink:#101115; --ink-soft:#56566E; --muted:#63637A;
  --bg:#F7F8FF; --card:#FFFFFF; --line:#E4E6F2; --line-strong:#C9CDE0;
  /* Form fields only. A text box on a near-white ground is identified
     by its border and nothing else, and WCAG 1.4.11 asks 3:1 of it;
     --line is 1.24:1 against white. Its own token because --line also
     draws card edges and rules, where a hairline is the correct look
     and the criterion does not apply -- darkening it globally would
     have fixed the fields by redesigning the site. 3.32:1 on white,
     3.14:1 on the page. */
  --field:#848CAB;
  /* #357CFF was 3.83:1 for white-on-blue AND blue-on-white, both under
     AA's 4.5. #2563EB is 5.17:1 in both directions. */
  --accent:#2563EB; --accent-soft:#E8F0FE;
  /* The DASHBOARD is back on the blue palette (owner 2026-08-18) after a day
     monochrome. These two tokens are how: every app surface that was greyed
     goes through them, so the revert is a repoint rather than thirty edits.
     They keep their names — --btn is "what an app action looks like" and
     --tint is "the soft surface behind app chrome", which is still true.
     The marketing pages do NOT use them; they use --cta, so the two sides can
     move independently. */
  --btn:var(--accent); --btn-hover:#1D4ED8;
  --tint:var(--accent-soft); --tint-line:#D6E2FD;
  --success:#0C7F3F; --success-bg:#E3F6EC;
  --warn:#96540A; --warn-bg:#FCF0DD;
  --danger:#C0272D;
  /* The chip pair for a dead end. --danger itself is the button red and lands
     around 3.3:1 on a pale ground, under AA for text this size, so the chip
     gets a darker ink of its own. */
  --danger-ink:#B3261E; --danger-bg:#FDECEC;
  /* the closing band on every marketing page, deepened to sit with the
     black actions rather than shout over them */
  /* Two faces, two jobs. Hanken carries every word a seller reads; Archivo is
     a sturdier, slightly narrower grotesque that gives headings a voice of
     their own instead of being the body face at a bigger size. It costs no
     extra request: it replaced Inter, which was being downloaded on every page
     purely as a fallback behind Hanken and so never actually rendered. */
  --sans:'Hanken Grotesk',system-ui,sans-serif;
  /* the face every value the app fills in is set in (2026-09-11) */
  --mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  /* Red Hat Display 700 since 2026-09-10, on the app as on the public pages
     (owner: "i mean the in-app side"). Loaded at 700 only. */
  --display:'Red Hat Display','Hanken Grotesk',system-ui,sans-serif;
  /* The radius scale, as tokens. These were 18/24px, and the sweep that put
     every numeric radius onto 6/4 skipped var() values — so anything styled
     through a token (.card, and with it the dashboard's Recent Activity and
     Connection panels) stayed at 24px. That was the last rounded thing left. */
  --r:0; --r-lg:0;
  --shadow:0 10px 30px rgba(30,40,120,.08);
  --shadow-lg:0 24px 60px rgba(30,40,120,.14);
  /* The marketing site's two front-door buttons — "Open the app" in the nav and
     "Start free" in the hero — are the brand blue again (owner 2026-08-17),
     matching the tile in the logo. Deliberately NOT --btn: inside the app,
     actions stay black, so blue means "this is Lotwize" on the way in and black
     means "this does something" once you are working. */
  --cta:#2563EB; --cta-hover:#1D4ED8;
}
/* Every pill on a public page is blue (owner 2026-08-17) — the nav's app
   button, "Get started for Free", "Send message". Scoped to body.mk / body.auth
   rather than applied to .btn-pill outright, because the same class is the
   dashboard's "List a headlight" and "List All Ready", and inside the app the
   actions stay black. The app's <body> carries no class, which is what makes
   the split expressible at all. */
body.mk .btn-pill,body.auth .btn-pill{background:var(--cta)}
body.mk .btn-pill:hover,body.auth .btn-pill:hover{background:var(--cta-hover)}
/* .light is the inverted pill used on dark surfaces; it must stay white */
body.mk .btn-pill.light,body.auth .btn-pill.light{background:#fff}
body.mk .btn-pill.light:hover,body.auth .btn-pill.light:hover{background:#E9E9EC}
*{box-sizing:border-box}
html{-webkit-text-size-adjust:100%;scroll-behavior:smooth;scroll-padding-top:96px}
@media (prefers-reduced-motion:reduce){html{scroll-behavior:auto}}
body{margin:0;font-family:var(--sans);color:var(--ink);background:var(--bg);
     font-size:16px;line-height:1.5;-webkit-font-smoothing:antialiased}
h1,h2,h3{margin:0;font-weight:600;line-height:1.15;font-family:var(--display);
  letter-spacing:-.015em}
a{color:var(--accent);text-decoration:none}
img{max-width:100%;display:block}
button{font:inherit;cursor:pointer;border:0;background:none;color:inherit}
::placeholder{color:var(--muted)}

/* ---- buttons ---- */
.btn-pill{display:inline-flex;align-items:center;gap:10px;background:var(--ink);
  color:#fff;border-radius:4px;padding:13px 22px;font-weight:600;
  font-size:16px;transition:transform .15s,box-shadow .15s}
.btn-pill:hover{transform:translateY(-1px);box-shadow:var(--shadow)}
/* No disc. It was blue originally, but the public pills are blue now, so a blue
   disc on a blue button is invisible — and the app overrides this rule anyway
   to sit the glyph inline. Restoring it would have changed nothing except
   adding a shape nobody can see. */
.btn-pill .ic{width:auto;height:auto;border-radius:0;background:none;
  display:inline-flex;align-items:center;justify-content:center;flex:none}
.btn-pill .ic svg{width:15px;height:15px;stroke:#fff}
.btn-pill.light{background:#fff;color:var(--ink);border:1px solid var(--line)}
.btn-pill.light .ic svg{stroke:var(--ink)}
/* Still called .btn-blue: it is the primary action, on ~40 elements across the
   app and the marketing pages, and renaming it would touch every one of them
   for no behavioural gain. A brightness filter did nothing useful on black, so
   hover is a real colour step now. */
.btn-blue{display:inline-flex;align-items:center;justify-content:center;gap:8px;
  background:var(--btn);color:#fff;border-radius:4px;padding:7px 16px;
  font-weight:600;font-size:14px;transition:background .15s}
.btn-blue:hover{background:var(--btn-hover)}
.btn-blue:disabled{background:var(--bg);color:var(--muted);cursor:default}
.btn-soft{display:inline-flex;align-items:center;justify-content:center;gap:8px;
  background:#fff;color:var(--ink-soft);border:1px solid var(--line);
  border-radius:4px;padding:8px 16px;font-weight:600;font-size:14px}
.btn-soft:hover{border-color:var(--line-strong)}
.icon-btn{width:32px;height:32px;border-radius:50%;border:0;
  background:var(--bg);display:inline-flex;align-items:center;justify-content:center;flex:none}
.icon-btn svg{width:14px;height:14px;stroke:var(--ink-soft)}

/* ---- chips / badges ---- */
.chip{display:inline-flex;align-items:center;gap:4px;border-radius:999px;
  padding:2px 8px;font-size:11px;font-weight:600}
.chip.chip-ok{background:var(--success-bg);color:var(--success)}
.chip.chip-warn{background:var(--warn-bg);color:var(--warn)}
.chip.chip-blue{background:var(--tint);color:var(--ink-soft)}
.chip svg{width:10px;height:10px;flex:none}

/* ---- cards / fields ---- */
.card{background:var(--card);border:1px solid var(--line);border-radius:var(--r-lg);
  box-shadow:var(--shadow)}
.field{display:flex;flex-direction:column;gap:6px;margin:0 0 16px}
.field>label{font-weight:600;font-size:14px}
.input,textarea.input,select.input{width:100%;border:1px solid var(--field);
  border-radius:6px;padding:10px 14px;font:inherit;font-size:14px;color:var(--ink-soft);
  background:#FBFBFE;outline:0;transition:border-color .12s,box-shadow .12s}
.input:focus{border-color:var(--line-strong);box-shadow:0 0 0 3px var(--tint)}
textarea.input{resize:vertical;min-height:116px}
#condition{min-height:67px}
/* The caret is the only thing that marks a select as a select, so it is a
   UI component under WCAG 1.4.11 and needs 3:1, not the 2.69:1 that
   #9A9AA8 gave it on the field fill. The colour is baked into the data
   URI below -- it will not follow a token. */
select.input{appearance:none;-webkit-appearance:none;padding-right:38px;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='%237A7A8C' 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 13px center;background-size:16px}
/* input with a static suffix (e.g. "3" + "%") */
.input.sfx{display:flex;align-items:center;gap:6px}
.input.sfx input{border:0;outline:0;background:none;font:inherit;color:var(--ink);width:100%;min-width:0}
.input.sfx span{color:var(--muted);font-weight:600}

/* ---- segmented control (New / Used / For Parts) ---- */
.seg-pill{display:inline-flex;background:#FBFBFE;border:1px solid var(--line);
  border-radius:4px;padding:4px}
.seg-pill .seg{border-radius:4px;padding:6px 16px;font-weight:600;
  font-size:14px;color:var(--muted)}
.seg-pill .seg.active{background:var(--accent);color:#fff}

/* ---- AI "Rewrite" chip-button ---- */
.rewrite-chip{display:inline-flex;align-items:center;gap:6px;background:var(--btn);
  color:#fff;border-radius:999px;padding:3px 10px;font-size:12px;font-weight:600}
.rewrite-chip svg{width:11px;height:11px}
.rewrite-chip img{width:11px;height:11px;display:block;filter:brightness(0) invert(1)}
.rewrite-chip:hover{filter:brightness(1.08)}

/* ---- misc shared ---- */
/* flex:none because a fixed-size round thing in a flex row is otherwise
   compressed by it. In the collapsed sidebar -- 64px, no room -- the ring came
   out an ellipse (owner 2026-09-05). */
.spinner{display:inline-block;flex:none;width:13px;height:13px;
  border:2px solid rgba(128,134,163,.35);
  border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite;
  vertical-align:-2px}
@keyframes spin{to{transform:rotate(360deg)}}
/* THE BOTTOM BAR (owner 2026-09-11, board D: "bottom bar is the best. but make it
   blue ... for notification type bottom blue pop ups, add the white expire state
   where it disappears ... For an error, make it a red box"). One transient for
   the whole app: blue, the toast's place, mono text, a white square with a blue
   tick on success, a white line along the foot that drains for the toast's
   life so the seller can see it is about to go. Errors are the same bar in
   red. Confirm dialogs are this bar too (see .cf-panel below). */
.toast{position:fixed;left:50%;bottom:0;transform:translateX(-50%) translateY(20px);
  width:min(560px,92vw);background:var(--accent);color:#fff;padding:11px 14px;border-radius:0;
  font:400 13px/1.5 var(--mono);display:flex;align-items:center;gap:12px;text-align:left;
  opacity:0;pointer-events:none;transition:.25s;z-index:90;overflow:hidden;box-sizing:border-box}
.toast.show{opacity:1;transform:translateX(-50%)}
.toast .tt{flex:1;min-width:0}
.toast::after{content:"";position:absolute;left:0;right:0;bottom:0;height:2px;background:#fff;
  transform-origin:left;transform:scaleX(0)}
.toast.show::after{animation:toast-drain var(--toast-ms,2.8s) linear forwards}
@keyframes toast-drain{from{transform:scaleX(1)}to{transform:scaleX(0)}}
.toast.success{background:var(--accent)}.toast.error{background:var(--danger)}
.toast .tk-sq{background:#fff}.toast .tk-sq svg path{stroke:var(--accent)}
.toast.error .tk-sq svg path{stroke:var(--danger)}
@media (prefers-reduced-motion:reduce){.toast.show::after{animation:none;transform:scaleX(1)}}
/* THE MARK (owner 2026-09-11, board B: "Square fills, tick pops. Make it blue,
   white check ... make the tick centered"). One confirmation glyph for the
   whole site: a blue square, a white tick that pops in. .tk-sq is the inline
   size; the identified banner's .idb-ic is the 42px one. */
.tk-sq{display:inline-grid;place-items:center;width:20px;height:20px;background:var(--accent);
  flex:none;vertical-align:-5px;border-radius:0}
.tk-sq svg{width:13px;height:13px;display:block;animation:tick-pop .3s cubic-bezier(.2,1.5,.4,1) both}
/* SQUARE ENDS, MITRED CORNER (owner 2026-09-11: "make sure that all check
   marks are the exact same ... I like the blocky look"). Round caps were the
   only thing making this mark disagree with .idb-ic and .cf-tick, which draw
   the same tick and never set them. */
.tk-sq svg path{fill:none;stroke:#fff;stroke-width:2.6;stroke-linecap:square;stroke-linejoin:miter}
@keyframes tick-pop{from{opacity:0;transform:scale(.3)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion:reduce){.tk-sq svg{animation:none}}
.muted{color:var(--muted)}
[hidden]{display:none!important}

/* ---- submissions (owner only) ---- */

/* ============ marketing pages ============ */
/* Short pages (terms, privacy, login…) left white below the footer. Flex column
   pins the dark footer to the bottom of the viewport instead (owner 2026-08-15). */
body.mk{background:#fff;min-height:100vh;display:flex;flex-direction:column}
/* the last section stretches, so short pages don't show a white band between
   the content and the footer */
body.mk>main{flex:1 0 auto;display:flex;flex-direction:column}
body.mk>main>:last-child{flex:1 0 auto}
/* contact's <main> IS the two-column grid — the rule above was overriding its
   display:grid, which stacked the page and left a dead gap (owner 2026-08-15) */
body.mk>main.mk-contact{display:grid}
body.mk>.mk-footer{flex:none;margin-top:auto}
/* One nav for every marketing page (owner 2026-08-15: the other pages didn't
   match the homepage). Geometry + the scrolled curve come from the homepage. */
/* Three columns, so the links sit on the page's true centre whatever is
   beside them. They used to be centred with margin:0 auto, which centres
   against the space LEFT OVER -- so adding a Log in link on the right
   pushed them off centre (owner 2026-09-02). */
.mk-nav{display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:34px;padding:10px max(24px,calc(50% - 609px));
  position:sticky;top:0;background:rgba(255,255,255,.85);backdrop-filter:blur(10px);z-index:20;
  transition:border-radius .28s cubic-bezier(.2,.8,.2,1),box-shadow .28s,background-color .28s}
/* scrolled: full width, curved along the bottom — the mirror of the footer's
   curved top. Never a narrow pill; the logo stays where it is. */
/* 26px -> 10px: on the page's radius scale, where 10px is a large surface. At 26
   the scrolled bar was the roundest thing left on the page. */
.mk-nav.floated{border-radius:0 0 6px 6px;background:rgba(255,255,255,.94);
  box-shadow:0 10px 34px rgba(16,17,21,.10)}
@media (prefers-reduced-motion:reduce){.mk-nav{transition:none}}
.mk-logo{height:48px}
.mk-links{display:flex;gap:54px;margin:0;justify-content:center}
.mk-actions{display:flex;align-items:center;justify-content:flex-end;gap:16px}
.mk-links a{color:var(--ink-soft);font-weight:500;font-size:16px}
.mk-links a:hover{color:var(--ink)}
/* The sparkle field is gone site-wide (owner 2026-08-15) — plain white reads
   cleaner and removes the fade-under-the-nav problem entirely. The -74px pull
   and its matching padding stay, so the hero still runs up behind the sticky
   bar and nothing shifts. */
.mk-hero{position:relative;text-align:center;margin-top:-74px;padding:162px 24px 110px;
  background:#fff}
.mk-hero h1{font-size:clamp(44px,7vw,76px);font-weight:600;letter-spacing:-.02em}
.mk-hero p{color:var(--ink-soft);font-size:16px;margin:18px 0 30px}
.mk-steps{background:var(--bg);padding:70px 6vw;border-radius:6px 6px 0 0;
  display:flex;flex-direction:column;gap:70px}
.mk-step{display:grid;grid-template-columns:1fr 1.25fr;gap:6vw;align-items:center;
  max-width:1200px;margin:0 auto;width:100%}
.mk-step.rev .mk-copy{order:2}
.mk-step img{border-radius:6px;box-shadow:var(--shadow-lg)}
.mk-num{font-size:44px;font-weight:700;color:var(--ink);display:block;margin-bottom:8px}
.mk-copy h2{font-size:34px;margin-bottom:12px}
.mk-copy p{color:var(--ink-soft);max-width:42ch}
.mk-band{background:var(--grad-blue);color:#fff;text-align:center;padding:84px 24px}
.mk-band h2{font-size:clamp(30px,4.5vw,44px)}
.mk-band p{opacity:.9;margin:14px 0 26px}
.mk-band .btn-pill{background:#fff;color:var(--ink)}
/* ---- skeletons ----
   Placeholders in the shape of the real content, so the wait shows its size and
   the layout doesn't jump when data lands. The shimmer is one shared keyframe. */

/* BLUE (owner 2026-09-11: "Make all the loading tints BLUE"), the same tint the
   title box shimmers with, and square like everything else since the 2026-09
   palette pass. */
.sk-box{background:linear-gradient(90deg,rgba(37,99,235,0) 25%,rgba(37,99,235,.13) 37%,rgba(37,99,235,0) 63%) #fff;
  background-size:400% 100%;animation:sk 1.3s ease-in-out infinite;border-radius:0}
@keyframes sk{0%{background-position:100% 0}100%{background-position:0 0}}
@media (prefers-reduced-motion:reduce){.sk-box{animation:none}}
.lst-card.sk{pointer-events:none}
.sk-thumb{height:150px;border-radius:6px;margin-bottom:12px}
.sk-line{height:13px;margin-bottom:8px}
.sk-line.short{width:55%}
.sk-btn{height:34px;width:100%;border-radius:4px;margin-top:12px}

/* phone-only action bar; hidden entirely on desktop, where the nav pill is
   always visible anyway */
.sticky-cta{display:none}
@media (max-width:640px){
  .sticky-cta{display:block;position:fixed;left:14px;right:14px;bottom:14px;z-index:40;
    background:var(--cta);color:#fff;text-align:center;font-weight:700;font-size:16px;
    padding:14px 18px;border-radius:4px;
    /* No shadow (owner 2026-08-31). It went through blue, neutral, and a
       four-stop fade before he asked for none -- the bar is a solid slab
       against the page and does not need lifting off it. */
    opacity:0;transform:translateY(calc(100% + 14px));pointer-events:none;
    transition:opacity .24s ease,transform .34s cubic-bezier(.22,1,.36,1)}
  /* It ARRIVES rather than blinking on. The scroll handler used to toggle
     `hidden`, which is display:none and cannot be transitioned -- so the class
     carries the state and `hidden` is cleared once, on init. Off-screen by its
     own height plus the 14px inset, so nothing shows mid-slide. */
  .sticky-cta.is-in{opacity:1;transform:none;pointer-events:auto}
  /* Still honoured: with no JS the bar never arrives, and a floating call to
     action that cannot be dismissed is worse than none. */
  .sticky-cta[hidden]{display:none}
}
@media (max-width:640px) and (prefers-reduced-motion:reduce){
  /* The slide is the decoration; appearing at all is the information. */
  .sticky-cta{transform:none;transition:opacity .18s linear}
}

/* Keyboard users otherwise tab through the whole nav on every page before
   reaching the content. Off-screen until focused. */
/* Visually gone, still read aloud. Clipped rather than display:none — a hidden
   live region is never announced at all. */
.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;
  overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
.skip-link{position:absolute;left:-9999px;top:0;z-index:100;background:var(--ink);color:#fff;
  padding:11px 18px;border-radius:0 0 6px 0;font-weight:600;font-size:14px}
.skip-link:focus{left:0}
/* A mouse click never draws a ring. Belt and braces with :focus-visible below:
   some elements (a div with tabindex, and <main>) take focus on click in a way
   that used to fall through to the browser's own outline. */
:focus:not(:focus-visible){outline:none}

/* Every element, not a list of six. The old rule named a, button, summary,
   input, select and textarea, so anything else that took focus -- the photo
   thumbs, and <main tabindex="-1"> which the skip link targets -- got Chrome's
   DEFAULT outline instead, which draws black and, on <main>, boxed the entire
   content area. */
:focus-visible{
  outline:2px solid var(--accent);outline-offset:2px;border-radius:4px}

/* <main> is a scroll target for the skip link, not a control. Landing on it
   should move the reader, not draw a box around the page. */
main:focus,main:focus-visible{outline:none}

/* ---- FAQ ---- native <details>, so it works with JS off and is keyboard
   operable for free; the marker is replaced with a rotating chevron. */
.mk-faq{background:#fff;padding:96px clamp(20px,5vw,64px) 104px}
.faq-wrap{max-width:820px;margin:0 auto}
.mk-faq h2{font-size:clamp(28px,3.4vw,40px);font-weight:600;margin:0 0 28px}
.faq-item{border-bottom:1px solid var(--line)}
.faq-item summary{list-style:none;cursor:pointer;padding:20px 44px 20px 0;position:relative;
  font-size:19px;font-weight:600;line-height:1.4}
.faq-item summary::-webkit-details-marker{display:none}
.faq-item summary::after{content:"";position:absolute;right:8px;top:26px;width:9px;height:9px;
  border-right:2px solid var(--muted);border-bottom:2px solid var(--muted);
  transform:rotate(45deg);transition:transform .22s ease}
.faq-item[open] summary::after{transform:rotate(-135deg)}
.faq-item summary:hover{color:var(--ink-soft)}
.faq-item p{margin:0 0 22px;color:var(--ink-soft);font-size:16px;line-height:26px;max-width:70ch}
@media (max-width:640px){
  .mk-faq{padding:56px 20px 64px}
  .mk-faq h2{font-size:26px;margin-bottom:16px}
  .faq-item summary{font-size:16px;padding:16px 36px 16px 0}
  .faq-item summary::after{top:22px}
  .faq-item p{font-size:16px;line-height:24px}
}

/* the contact form hands off to a mail client, so it has to say so */
.cf-promise{margin:12px 0 0;font-size:13px;color:var(--muted)}
.cf-done{margin-top:14px;padding:13px 15px;border-radius:6px;background:var(--success-bg);
  border:1px solid rgba(24,169,87,.3);display:flex;flex-direction:column;gap:5px;
  font-size:14px;line-height:21px}
.cf-done strong{font-weight:700}

/* Rounded when the footer meets page content directly; square when a coloured
   band sits above it, because the band already carries the curve (owner). */
/* The closing block is WHITE (owner 2026-09-04: "I find the bottom page with
   the abrupt color change to be awkward. Revert to white").

   Every section above it is white separated by a hairline, and the deep blue
   plate arrived under the last of them with no transition — the join read as
   an artefact rather than a close. So the footer joins that system: white
   ground, one hairline at the top, and no radius, because a rounded corner
   over white is a corner nobody can see. */
.mk-footer{position:relative;overflow:hidden;background:#fff;
  border-top:1px solid var(--line);
  color:var(--ink);text-align:center;padding:70px 24px 40px;
  border-radius:0;display:flex;flex-direction:column;align-items:center;gap:28px}
.mk-footer.flush{border-radius:0}
/* No watermark. The cta-shapes smear was drawn for the blue plate and was the
   last thing on the footer still arguing for one (owner 2026-09-04: "remove
   the watermark on the left side of the screen"). The block is the lockup, the
   nav and the legal line on white. */
.mk-footer>*{position:relative;z-index:1}

.mk-footer img{height:56px}
/* The chip was white on blue. On white it needs the tint to read as a chip at
   all, and the links their own ink. */
.mk-footer nav{display:flex;gap:8px;background:var(--bg);border-radius:6px;padding:6px}
.mk-footer nav a{color:var(--ink);font-weight:600;font-size:14px;padding:8px 22px;border-radius:4px}
.mk-footer nav a:hover{background:#fff}
.mk-footer p{color:#63637A;font-size:14px;margin:0}
@media (max-width:860px){.mk-step{grid-template-columns:1fr}.mk-step.rev .mk-copy{order:0}
  .mk-nav{display:flex;flex-wrap:wrap;align-items:center;gap:14px;padding:16px 20px}
  /* The grid's third column anchored these to the right edge; a flex row does
     not, so they have to claim the space themselves or the whole bar clumps
     left with the logo and leaves the right half empty. */
  .mk-nav .mk-actions{margin-left:auto}
  .mk-links{order:3;width:100%;justify-content:center;gap:20px;margin:0}}

/* ============ phones (iPhone SE 375 → Pro Max 430) ============
   At 390px the desktop chrome overflowed the viewport — the logo lockup plus a
   192px-min pill is wider than the screen, so the page scrolled sideways and
   every section inherited the shift. These rules shrink the chrome to fit and
   let long words break rather than push the layout. Page-specific geometry
   lives in each page's own <style>, which loads after this file. */
@media (max-width:640px){
  /* nothing may exceed the viewport: a single overflowing child scrolls the
     whole document sideways and misaligns everything above it */
  body.mk{overflow-x:hidden}
  body.mk img,body.mk svg{max-width:100%}
  h1,h2,h3,p,a{overflow-wrap:break-word}

  .mk-nav{display:flex;gap:10px;padding:10px 16px}
  .mk-logo{height:36px}
  .mk-links{gap:26px}
  .mk-links a{font-size:16px}
  /* the pill is the widest fixed thing in the bar — drop its min-width and let
     it size to its label */
  .btn-pill{min-width:0;font-size:14px;padding:4px 17px 4px 4px;gap:9px}
  /* The links wrap to their own row, so without this the actions sit right
     next to the logo instead of anchoring the right end of the bar.
     This named `.mk-nav>.btn-pill` until 2026-09-03 and matched NOTHING: the
     pill is inside .mk-actions on every public page, so the fix was written
     and never applied. Both selectors now, since a page may hold either
     shape. */
  .mk-nav>.btn-pill,.mk-nav .mk-actions{margin-left:auto}
  /* Two actions plus a lockup is tight at 375px; the gap gives first. */
  .mk-nav .mk-actions{gap:10px}
  .btn-pill .ic{width:34px;height:34px}
  .btn-pill .ic svg{width:14px;height:14px}

  .mk-hero{margin-top:0;padding:84px 20px 56px}
  .mk-hero h1{font-size:clamp(32px,9vw,40px)}
  .mk-hero p{font-size:16px}
  .mk-steps{padding:56px 20px;border-radius:6px 6px 0 0;gap:48px}
  .mk-band{padding:60px 20px}

  .mk-footer{padding:52px 20px 30px;border-radius:0;gap:18px}
  .mk-footer img{height:40px}
  /* the pill nav is a single flex row on desktop; it has to wrap on a phone */
  .mk-footer nav{flex-wrap:wrap;justify-content:center;gap:4px;padding:6px;
    border-radius:6px;max-width:100%}
  .mk-footer nav a{font-size:14px;padding:8px 14px}
  .mk-footer p{font-size:13px}
}

/* ============ contact page ============ */
.mk-contact{display:grid;grid-template-columns:1fr 1.1fr;gap:6vw;max-width:1200px;
  margin:0 auto;padding:70px 24px 110px;background:transparent}
body.mk.contact{background:#fff}  /* white, so the star field reads like every other page */
.mk-c-left h1{font-size:clamp(40px,6vw,64px);font-weight:600}
.mk-c-left>p{color:var(--ink-soft);font-size:16px;margin:14px 0 34px}
.mk-email-card{background:var(--ink);color:#fff;border-radius:6px;padding:28px 30px}
.mk-email-card h3{font-size:22px;margin-bottom:10px}
.mk-email-card p{opacity:.88;font-size:14px;margin:0 0 20px}
.mk-email-card a{display:inline-flex;align-items:center;gap:11px;color:#fff;font-weight:700}
.mk-email-card a .ic{width:38px;height:38px;border-radius:50%;background:#fff;
  display:inline-flex;align-items:center;justify-content:center}
.mk-email-card a .ic svg{width:17px;height:17px;stroke:var(--ink)}
.mk-c-form{padding:30px}

/* ============ app shell ============ */
/* THE TWO-COLUMN PAGE SHELL, shared by the dashboard and billing. Agreed with
   the other session 2026-09-08 rather than each of us minting one: two pages
   differing by a gutter and a breakpoint is the kind of drift nobody spots
   until the owner does.

   320px matches the comps panel and the settings rail, so the app has one
   rail width. 1050px is the breakpoint .nl-photos already un-stickies at, so
   this inherits a number instead of adding a second.

   minmax(0,1fr) on the main column, NOT 1fr: a grid track defaults to
   min-content, so a wide table or a long unbroken string pushes the side
   column off the screen instead of scrolling inside its own column. Both
   pages have tables. */
.page-cols{display:grid;grid-template-columns:minmax(0,1fr) 320px;gap:24px;
  align-items:start}
.page-main{min-width:0}
.page-side{display:grid;gap:16px;position:sticky;top:24px}
@media (max-width:1050px){
  .page-cols{grid-template-columns:1fr}
  .page-side{position:static}}

.shell{display:grid;grid-template-columns:236px 1fr;min-height:100vh;
  transition:grid-template-columns .26s cubic-bezier(.2,.8,.2,1)}
/* Collapsed: the rail keeps its icons and gives the listing back ~170px.
   The nav does NOT unmount -- hover re-expands it over the content instead of
   pushing it, so nothing reflows under the pointer while you are reading. */
.shell.side-collapsed{grid-template-columns:64px 1fr}
/* Explicit, because the collapsed rail leaves the flow: without this the main
   column becomes the first in-flow grid item and slides under the rail. */
.shell > .main{grid-column:2}
/* `side-peek` is added by JS on pointer/focus enter. It is not a :hover rule:
   with the rail fixed-positioned, an explicit width was ignored no matter how
   it was applied, so the expansion drives the GRID -- the same mechanism the
   collapse itself uses, which works. */
.shell.side-collapsed .side{padding:38px 0 24px;align-items:center}
.side,.side .tab{transition:padding .26s cubic-bezier(.2,.8,.2,1)}
@media (prefers-reduced-motion:reduce){
  .shell,.side,.side .tab,.side .lbl,.side-collapse svg{transition:none}}
.shell.side-collapsed.side-peek{grid-template-columns:236px 1fr}
.shell.side-collapsed.side-peek .side{padding:38px 24px 24px;align-items:stretch}
.side-nav{overflow:hidden}
.side .lbl{white-space:nowrap;
  transition:opacity .16s ease,max-width .26s cubic-bezier(.2,.8,.2,1)}
.shell.side-collapsed .side .lbl{opacity:0;max-width:0;overflow:hidden}
/* A spinner's label follows the same rule as a tab's own. "Signing out…" was a
   bare text node in the tab, so the line above could not reach it and the word
   appeared in a 64px rail. Peeking widens the rail, so the word is allowed
   back by the same selector rather than by a second piece of state. */
.shell.side-collapsed:not(.side-peek) .side .sp-lbl{display:none}
.shell.side-collapsed .side .side-logo{display:none}
/* Aligned with the nav icons rather than pinned to the rail's left edge
   (owner 2026-09-03, on the collapsed rail: "the PFP at the bottom left of my
   screen"). Measured before: icons centred at cx 19, the tile at cx 15 -- four
   pixels out, which is exactly the amount that reads as a mistake rather than
   as a decision. The tabs above it are justify-content:center in this state,
   so centring the card is what makes the whole rail one column instead of a
   column with something leaning out of the bottom of it. */
.shell.side-collapsed .side .conn-card{margin-top:auto;align-self:center;
  margin-left:0;padding:0;border:0;background:none;gap:0}
.shell.side-collapsed .side .conn-card .conn-txt{display:none}
.shell.side-collapsed.side-peek .side .lbl{opacity:1;max-width:160px}
.shell.side-collapsed.side-peek .side .side-logo{display:revert}
.shell.side-collapsed.side-peek .side .conn-card{align-self:auto;margin-left:0;
  padding:10px;border:1px solid var(--line);background:var(--bg);gap:10px}
.shell.side-collapsed.side-peek .side .conn-card .conn-txt{display:flex}
.shell.side-collapsed .side .tab{justify-content:center;padding:8px;gap:0}
.shell.side-collapsed.side-peek .side .tab{justify-content:flex-start;
  padding:8px 12px;gap:13px}
/* In the rail's own flow, directly above Dashboard (owner 2026-09-03).
   It was absolutely positioned: first inside the rail at top:14px, where
   collapsing moved it to the middle and it landed ON the Dashboard tab, then
   floating on the border, which stopped the collision but read as a separate
   control stuck to the panel. In flow it cannot overlap anything by
   construction, and it shares the nav's alignment: right-aligned beside the
   tabs when open, centred over their icons when collapsed. */
/* Built from .side-nav .tab rather than beside it (owner 2026-09-03:
   "integrate with the sidebar basically"). It was a 28x28 bordered white
   square floating above a column of full-width borderless rows -- the only
   control in the rail that looked like a control. Same width, same height,
   same corner, same padding, so it reads as the row above Dashboard rather
   than as a widget parked over it.

   Blue on hover, which the tabs are not: theirs is the grey --bg and blue is
   reserved for the ACTIVE tab. The owner asked for the blue rectangle, and it
   is defensible here for a reason the tabs do not have -- this row is never
   active, so blue can mean "pressable" without ever colliding with blue
   meaning "you are here". */
.side-collapse{display:flex;align-items:center;gap:13px;padding:8px 12px;
  min-height:40px;border-radius:4px;width:100%;flex:none;
  border:0;background:none;color:var(--ink-soft);cursor:pointer;
  margin:0 0 4px;transition:background .15s,color .15s}
.side-collapse:hover{background:var(--accent-soft);color:var(--accent)}
/* Collapsed it takes the collapsed tab's own geometry, so it sits directly
   above the house icon on the same column at the same size. */
/* Sized by its icon like a collapsed tab, and matched to one: the tab measures
   42px wide in this state, so the button carries the same horizontal padding
   rather than the 8px its own icon would need. Same width, same height, same
   column, directly above the house. */
.shell.side-collapsed .side-collapse{justify-content:center;padding:8px 12px;
  gap:0;width:auto}
.shell.side-collapsed.side-peek .side-collapse{justify-content:flex-start;
  padding:8px 12px;gap:13px;width:100%}
.side-collapse:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.side-collapse svg{width:18px;height:18px;flex:none;
  transition:transform .26s cubic-bezier(.2,.8,.2,1)}
.shell.side-collapsed .side-collapse svg{transform:scaleX(-1)}

.side{overflow:visible;background:#fff;border-right:1px solid var(--line);padding:38px 24px 24px;
  display:flex;flex-direction:column;gap:8px;position:sticky;top:0;height:100vh}
/* 22px left the mark small inside a 236px rail (owner 2026-08-15); 46px still
   read small once the wordmark replaced the lockup (owner 2026-09-11). 54px puts
   the letters at ~40px and the box at 182px inside the rail's 188px. */
.side-logo img{height:54px;width:auto;margin:0 auto 26px}
.side-nav{display:flex;flex-direction:column;gap:5px}
/* A NAV ENTRY THE ACCOUNT CANNOT USE (owner 2026-09-07: "once the user has
   cancelled their subscription and once it expires, the new listing page
   should be gone. (or greyed out)"). Greyed rather than removed: a tab that
   vanishes leaves a seller looking for the thing they used yesterday, and this
   one carries a title saying what to do about it. Still focusable, so a
   keyboard reaches the explanation. */
.nav-off{opacity:.45;cursor:default}
.nav-off:hover{background:transparent}

.side-nav .tab{display:flex;align-items:center;gap:13px;padding:8px 12px;min-height:40px;
  border-radius:4px;color:var(--ink-soft);font-weight:600;font-size:16px;cursor:pointer}
.side-nav .tab svg{width:18px;height:18px;flex:none}
.side-nav .tab:hover{background:var(--bg)}
.side-nav .tab.active{background:var(--accent-soft);color:var(--accent)}
/* The exit survey in the delete dialog. Quiet on purpose: it sits above a red
   button and must not compete with it for attention. */
.dz-why{border:0;padding:0;margin:0 0 16px}
.dz-why-row{display:flex;align-items:center;gap:9px;margin:7px 0;
  font-size:14px;color:var(--ink-soft);cursor:pointer}
.dz-why-row input{width:15px;height:15px;flex:none;accent-color:var(--accent)}
/* The acknowledgement, which is not one of the optional reasons above it and
   must not read as a seventh checkbox in the same list. It is the thing that
   arms the button, so it is separated and set in the body ink rather than the
   quieter reason text. */
.cx-ack{margin:16px 0 4px;padding-top:14px;border-top:1px solid var(--line);
  align-items:flex-start;font-weight:600;color:var(--ink)}
.cx-ack input{margin-top:3px}
.dz-note{margin-top:9px;min-height:64px;resize:vertical}
.conn-card{margin-top:auto;display:flex;align-items:center;gap:10px;
  flex-wrap:nowrap;border:1px solid var(--line);
  border-radius:6px;padding:10px;cursor:pointer;font-size:13px}
/* A seat has no Settings to open, so the card is a readout rather than a
   control and must not offer a pointer it cannot honour. */
.conn-card.static{cursor:default}
/* The one time an invite code is visible. Monospace and selectable, because
   the admin has to copy it accurately into a message. */
.sec-code-out{margin:8px 0;padding:10px 12px;border:1px solid var(--line);
  border-radius:6px;background:var(--bg);font-size:14px;user-select:all;
  overflow-wrap:anywhere}
.sec-code-out code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
/* Expand sits at the right of the field heading, next to Rewrite. */
.fhead .nl-expand{margin-left:auto}
/* The makes the country of origin is stated on. A wrapping grid rather than a
   long column: forty of them read as a wall in one line each. */
.ts-makes{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));
  gap:2px 14px;max-height:210px;overflow-y:auto;padding:10px 12px;
  border:1px solid var(--line);border-radius:6px;background:var(--card)}
.ts-make{display:flex;align-items:center;gap:7px;font-size:13px;padding:3px 0;
  cursor:pointer}
.ts-make input{flex:none}
/* A seat's drafts, as a folder on the admin's Drafts page. Closed by default:
   the admin's own work is what they came for, and a stack of open grids would
   bury it. */
.dr-group{margin-top:18px;border-top:1px solid var(--line);padding-top:14px}
.dr-group > summary{display:flex;align-items:baseline;gap:10px;cursor:pointer;
  list-style:none;padding:6px 0;font-size:14px}
.dr-group > summary::-webkit-details-marker{display:none}
.dr-group > summary::before{content:"";width:0;height:0;flex:none;
  border-left:5px solid var(--muted);border-top:4px solid transparent;
  border-bottom:4px solid transparent;transform:translateY(1px);
  transition:transform .12s ease}
.dr-group[open] > summary::before{transform:rotate(90deg) translateX(1px)}
.dr-group > summary:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.dr-group .dr-who{font-weight:600;color:var(--ink)}
.dr-group .dash-grid{margin-top:12px}
/* Who listed what. Its own rows rather than the 2FA card's: those are capped
   at 420px, which left the numbers floating in the middle of a full-width
   card instead of lining up in columns. */
#dash-seat-head{margin-top:4px}
/* Not a toast: toast() clears after 2.8s, which is not long enough to read a
   sentence about your own photos having been rewritten. It stays until the
   seller dismisses it or leaves the listing. */
/* The anchor checklist. 23 tokens flat is a wall, so they are grouped and the
   control is a three-state chip rather than a checkbox -- a checkbox cannot
   say "maybe" and that is the state the anchor set most often carries. */
/* "Needs review" sits beside Ready/Missing info rather than replacing it: a
   draft can be short of information AND carry an unread part number, and
   collapsing the two would hide one of them. */
.chip-review{background:var(--warn-bg);color:var(--warn)}
.draft-search{display:flex;gap:10px;align-items:center}
.draft-search #draft-triage{flex:none;width:auto;min-width:190px}
/* Compat-ruled-out: dimmed and struck, never hidden. Hiding would shorten the
   list under the cursor AND cost the model's offer-surface metric, which counts
   a token that REACHES the seller. Still clickable on purpose. */
.tcp-opt.tcp-ruled{opacity:.45}
.tcp-opt.tcp-ruled .tcp-name{text-decoration:line-through}
/* Dimmed, NOT struck through. A struck row that accepts the click and swaps
   reads as a broken control; the strike is reserved for compat, which
   refuses. Kept lighter than .tcp-ruled's .45 so the two are separable at a
   glance (owner 2026-08-31, "Replace is better"). */
.tcp-opt.tcp-swaps{opacity:.7}
.title-choice-derived{display:inline-flex;align-items:center;gap:6px;
  background:var(--success-bg);color:var(--success);border-radius:999px;
  padding:2px 8px;font-size:12px;font-weight:600;margin:0 4px}
.tcd-undo{background:none;border:0;padding:0;font:inherit;color:inherit;
  text-decoration:underline;cursor:pointer}
/* auto-FILL where .stat-strip is auto-fit, and THAT IS THE ONLY
   DIFFERENCE. auto-fit collapses empty tracks, so two seats stretched
   to 556px each against the figures' 208px, with the chip stranded at
   the far edge -- the disconnect the old table had, rebuilt in a new
   shape (measured 2026-08-29). The floor tracks .stat-strip's 150px
   rather than the 180 it was written against, so a seat is exactly a
   figure, which is what the ruling asked for. */
#dash-by-seat{grid-template-columns:repeat(auto-fill,minmax(150px,1fr))}
/* A seat IS a .stat -- same card, numerals and chip -- so the only rule it
   needs of its own is the address. .tl wraps by default, and a two-line email
   would push that seat's number off the baseline it shares with every figure
   on the page. The full address stays in the title attribute, because two
   seats at one company can truncate to the same string. */
#dash-by-seat .tl{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* The model output itself, readable without a clipboard round trip. Capped
   and scrolled: a full label dump is longer than the panel above it, and the
   curated reading is what the eye should land on first. */
.mdj{max-height:300px;overflow:auto;margin:0 0 12px;padding:10px 12px;
  background:var(--surface-2,#F5F6FA);border:1px solid var(--line);
  border-radius:6px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
  font-size:12px;line-height:1.5;color:var(--ink);white-space:pre;tab-size:2}
/* Seat rows span the card. They used to borrow .sec-method, which is capped
   at 420px for the 2FA panel, so the count and Remove sat in the middle of a
   full-width card instead of at its edge. */
.seat-row{display:grid;grid-template-columns:1fr auto auto auto auto;gap:16px;
  align-items:baseline;padding:10px 0;border-bottom:1px solid var(--line);
  font-size:14px}
.seat-row:last-child{border-bottom:0}
.seat-who{color:var(--ink);overflow-wrap:anywhere}
.seat-n{font-variant-numeric:tabular-nums;white-space:nowrap}
.seat-status{white-space:nowrap}
/* The base .chip carries no ground of its own, so a plain one sat as bare
   text between two coloured pills. All three states are the same KIND of
   thing and should read that way. */
.seat-status .chip{background:var(--bg);color:var(--muted)}
.seat-status .chip.chip-ok{background:var(--success-bg);color:var(--success)}
.seat-status .chip.chip-danger{background:var(--danger-bg);color:var(--danger-ink)}
/* An expired code is not a warning, it is a dead end: the admin has to act. */
.chip-danger{background:var(--danger-bg);color:var(--danger-ink)}
.seat-status .chip{font-variant-numeric:tabular-nums}
/* The button sits next to the field at its own width; in a two-column grid it
   stretched into a banner. */
.seat-add{display:flex;align-items:flex-end;gap:12px;flex-wrap:wrap;margin-top:16px}
.seat-add .field{flex:1 1 260px;max-width:340px;margin:0}
/* The invite button is the input's TWIN here, not a button that happens to sit
   next to one. Owner 2026-09-09: "align the 'send invite' button with the text
   box."

   align-items:flex-end already put their bottoms on one line; the mismatch is
   height. .input is 10px of padding with a 1px border top and bottom, .btn-blue
   is 7px and no border, so the button stood 8px shorter and read as sitting
   low beside the box. Matching the padding and adding a transparent border
   gives the two the same box, rather than pinning a height that would go stale
   the moment either changes. */
.seat-add .btn-blue{padding-top:10px;padding-bottom:10px;
  border:1px solid transparent}
/* ROOM FOR THE FOCUS RING, so selecting the box moves nothing. Owner
   2026-09-09: "When the 'add a seat box' is selected, it isn't aligned with the
   'send an invite button'", and "make sure the bottom line separating the text
   box and the 'invite sent..' doesn't move."

   .input:focus draws a 3px box-shadow ring. A shadow takes no space, so the
   ring spilled 3px past the input on every side: below the button's edge,
   which reads as the pair falling out of line the moment you click into the
   box, and down onto the .pn-where rule under the row. Reserving the three
   pixels keeps both the pairing and the divider still, in both states, without
   pinning a height that would go stale the moment either element changes. */
.seat-add{padding-bottom:3px}
/* And the divider stays put when the panel's own words change -- "Invite sent
   to ..." and the could-not-email fallback are different lengths, and the rule
   sits at the TOP of that block, so it must not be what tells the eye which
   one it got. */
#seats-code.pn-where{margin-top:0}
@media (prefers-reduced-motion:reduce){.dr-group > summary::before{transition:none}}
/* min-height:60px sized this for two lines. It holds one now, so the height
   comes from the 30px avatar and its padding instead of leaving a gap above and
   below the name. */
/* Account initials instead of a generic status dot (owner 2026-08-15). eBay
   exposes no seller avatar, so the initials stand in for one. */
/* Square, on the app's 6px scale (owner 2026-08-18). It was the last circle in
   the sidebar; round reads as a photo, and there is no photo — eBay exposes no
   seller avatar, so this is initials in a tile. */
.conn-card .dot{width:30px;height:30px;border-radius:6px;flex:none;
  display:grid;place-items:center;background:var(--line);color:var(--ink-soft);
  font-size:12px;font-weight:700;letter-spacing:.02em}
.conn-card.on .dot{background:var(--accent);color:#fff}
.conn-card.sandbox .dot{background:var(--warn);color:#fff}
/* Tile hard left, status and username hard right, so the card is balanced
   rather than left-clumped with dead space after it (owner 2026-08-18). */
.conn-card .conn-txt{display:flex;flex-direction:column;min-width:0;line-height:1.25;
  flex:1 1 auto;margin-left:auto;text-align:right;align-items:flex-end}
/* The username carries this card now, so it gets the size — and it truncates
   rather than pushing the rail wider (the full name is on the card's title). */
.conn-card #conn-text{font-weight:700;font-size:14px;min-width:0;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%}
/* Two clean lines — status over username — and the username truncates instead
   of pushing the card wider. The PRODUCTION tag is dropped (it's the normal
   case and it overflowed); SANDBOX still shows, because listing to the wrong
   environment matters (owner 2026-08-15). */
.conn-card .who{color:var(--muted);display:block;min-width:0;font-size:12px;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.conn-card .who .tag{display:none}
.conn-card.sandbox .who .tag{display:inline;margin-left:5px;font-size:11px;
  font-weight:700;letter-spacing:.04em;color:var(--warn)}
.main{padding:46px 24px 40px;min-width:0}
.main .card{border-radius:6px;box-shadow:none}
@media (max-width:860px){.mk-contact{grid-template-columns:1fr}}

/* ---- revise as a sub-page ----
   Revise is a sub-page of Listings (owner 2026-08-26): the grid is replaced in
   place by the editor under its own heading, the Listings tab stays lit, and
   Back returns to the grid. It reads as somewhere you went inside Listings, not
   a window over the app. The editor node is MOVED here, so every control is the
   same element doing the same job as in New Listing. */
.rv-head{align-items:center}
/* .page-head is a flex row; without this the title block shrinks to its content
   and the sub-line wraps at half the page width with empty space beside it */
.rv-head>div{flex:1;min-width:0}
.rv-back{display:inline-flex;align-items:center;gap:7px;margin-left:auto;align-self:center;
  white-space:nowrap}
.rv-back svg{width:15px;height:15px;flex:none}
#rv-sub{max-width:70ch}

/* Revise runs inside the New Listing editor, so all it needs of its own is a
   banner saying which live listing is being edited. */
.rv-banner .btn-soft{margin-left:auto;flex:none;padding:6px 14px;font-size:13px}

/* Live-listings cards carry three actions, which wrapped to two lines in the
   200px dashboard card. Wider cards + no wrap keeps each button on one line. */
#lst-grid{grid-template-columns:repeat(auto-fill,minmax(310px,1fr))}
#lst-grid .pa{flex-wrap:nowrap}
#lst-grid .pa .btn-blue,#lst-grid .pa .btn-soft{white-space:nowrap;flex:0 0 auto;
  padding:7px 13px;font-size:13px}
#lst-grid .pa .btn-blue{flex:1 1 auto}
/* full titles: wrap to as many lines as needed, never clipped */
#lst-grid .pt{min-height:0;overflow:visible;white-space:normal;
  word-break:break-word;line-height:18px}
#lst-grid{align-items:start}

/* live-listings filter bar: native selects, so the option list is a compact
   scrolling popup rather than anything that takes over the page */
.lst-filters{display:flex;flex-wrap:wrap;align-items:end;gap:12px;margin:0 0 14px}
.lst-filters label{display:flex;flex-direction:column;gap:5px;font-size:12px;
  font-weight:600;color:var(--ink-soft)}
.lst-filters select.input{width:auto;min-width:150px;max-width:230px;
  padding:8px 32px 8px 12px;font-size:14px;color:var(--ink)}
.lst-filters select.input:disabled{color:var(--muted);cursor:default}
.lst-filters .btn-soft{align-self:end}
/* the other filters stack a caption over a control; this one is a checkbox and
   its label belongs beside it, so it opts out of the column layout */
.lst-filters label.lst-only-rev{flex-direction:row;align-items:center;gap:8px;
  align-self:end;height:37px;font-size:13px;cursor:pointer;
  border:1px solid var(--line);border-radius:4px;padding:0 12px;background:#fff}
.lst-filters label.lst-only-rev:hover{border-color:var(--line-strong)}
.lst-filters label.lst-only-rev input{accent-color:var(--accent);
  width:15px;height:15px;cursor:pointer}
.lst-filters label.lst-only-rev:has(input:disabled){opacity:.55;cursor:default}
#lst-shown{color:var(--accent);font-weight:600;margin-left:6px}

/* live-listings toolbar */
.lst-hidden{margin:0 0 14px;font-size:13px}
.lst-sort{display:inline-flex;align-items:center;gap:8px;font-size:13px;
  font-weight:600;color:var(--ink-soft);white-space:nowrap}
.lst-sort select.input{width:auto;min-width:186px;padding:8px 34px 8px 12px;font-size:14px}

/* ============ dashboard ============ */
.page-head{display:flex;align-items:flex-start;gap:20px;margin-bottom:24px}
.page-head h1{font-size:32px;line-height:38px}
.page-head p{margin:5px 0 0}
.page-head .muted{color:var(--ink-soft)}
.page-head .btn-pill{margin-left:auto;align-self:center}
/* The arrow to /ops, root only. It takes the auto margin so the pill after
   it sits beside it; hidden, the pill has the margin back. */
.page-head .ops-link{margin-left:auto;align-self:center;display:inline-flex;align-items:center;gap:6px;
  font-weight:600;font-size:14px;color:var(--accent);text-decoration:none;white-space:nowrap}
.page-head .ops-link svg{width:16px;height:16px}
.page-head .ops-link:hover{text-decoration:underline}
/* A VISIBLE link only (2026-09-11 sweep). The link is hidden for every seller
   but the owner, and a hidden element still matches `~`: this rule was taking
   the chip's margin-left:auto away on every seller's dashboard, so "Choose a
   plan" and "List a headlight" sat against the greeting instead of the edge. */
.page-head .ops-link:not([hidden]) ~ .btn-pill,.page-head .ops-link:not([hidden]) ~ .bal-chip{margin-left:12px}
/* The balance, top right of every page that spends it (owner 2026-09-03).
   It takes the margin-left:auto so it sits at the right edge; whatever action
   follows it gives up its own auto and sits alongside instead of fighting for
   the same space. */
.bal-chip{display:inline-flex;align-items:center;margin-left:auto;
  align-self:center;white-space:nowrap;
  font-weight:600;color:var(--ink-soft);
  background:var(--card);border:1px solid var(--line);
  /* Sized to .btn-pill, measured against the rule that actually wins.
     .btn-pill is declared twice: the early rule is padding:4px 20px 4px 4px
     radius:999, and a later GLOBAL override replaces it with
     `border-radius:4px;padding:13px 22px`. The chip was built against the
     first one, which is why it came out about 27px beside a 45px button.
     Matching the winner: same font size, same 4px corner, and 1px less
     padding on each side to pay for the border the pill does not have. */
  font-size:16px;border-radius:4px;padding:12px 21px;cursor:pointer;
  transition:border-color .15s,color .15s}
.bal-chip:hover{border-color:var(--line-strong);color:var(--ink)}
/* THE CHIP REACHES THE EDGE OF THE HEAD. Owner 2026-09-08: "The feature box
   should extend all the way to the end of the barrier." It was sized to a
   pill and floated at the right; a minimum width lands its edge on the
   content column's, so the head reads as one bar ending in one box rather
   than a heading with a tag stuck after it. */
.bal-chip{position:relative;min-width:210px;justify-content:space-between}
/* A chip with no figure in it sizes to its words. The min-width above exists so
   a changing money value does not make the chip twitch; a fixed phrase has no
   such problem and was left sitting in a box two thirds empty. */
.bal-chip.solo{min-width:0;justify-content:center}

/* THE DETAIL, ON HOVER AND ON FOCUS. Focus as well as hover is not optional:
   the chip is a button, so it is reachable by keyboard, and a panel that only
   answers to a mouse is one a keyboard can select but never read.

   A true overlay, so a shadow is allowed here -- the design system forbids
   them AT REST, which this is not. */
/* THE SAME WIDTH AS THE CHIP, BOTH EDGES. Owner 2026-09-08: "adjust the
   horizontal length of the box to be equal on both sides."

   It was anchored right:0 with min-width:100%, so it lined up with the chip's
   right edge and grew leftwards past it whenever a row was wider than the
   chip -- an overhang on one side only. Pinned to both edges it is exactly the
   chip's width, and the rows wrap instead of pushing the box sideways. */
.bal-pop{position:absolute;top:calc(100% + 6px);left:0;right:0;z-index:30;
  display:none;
  background:var(--card);border:1px solid var(--line);border-radius:var(--r);
  box-shadow:0 8px 24px rgba(20,23,28,.14);padding:10px 12px;
  font-size:13px;font-weight:400;text-align:left;cursor:default}
.bal-chip:hover .bal-pop,
.bal-chip:focus-visible .bal-pop{display:block}
/* Wraps rather than nowrapping, now that the box cannot widen to fit. */
.bal-pop-row{display:flex;justify-content:space-between;gap:18px;
  padding:4px 0;color:var(--muted)}
.bal-pop-row b{color:var(--ink);font-weight:600;
  font-variant-numeric:tabular-nums}
/* Coarse pointers have no hover: the panel would either never appear or stick
   after a tap. The chip already goes to Billing, which is where all of this
   lives in full. */
@media (hover:none){.bal-pop{display:none !important}}
/* "Balance" carries the meaning, the number carries the weight. A bare
   "$4.70" beside "List a headlight" reads as the price of listing one. */
.bal-k{color:var(--muted);margin-right:8px}
.bal-v{color:var(--ink);font-variant-numeric:tabular-nums}
.bal-chip[hidden]{display:none}
.page-head .bal-chip + .btn-pill,
.page-head .bal-chip + .drafts-tools{margin-left:12px}
/* THE FIGURE ROW. Five tiles at 33px each said everything was equally worth
   knowing, which is the same as saying none of it was. One lead figure, then
   the rest at a supporting size.

   auto-fit with a 150px floor so four or five sit on one line on a laptop and
   wrap in pairs on a narrow window, instead of the 180px floor that dropped
   the fifth onto a line of its own and left it looking like an afterthought. */
.stat-strip{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));
  gap:16px;margin-bottom:24px;align-items:stretch}
.stat{background:#fff;border:1px solid var(--line);border-radius:6px;
  padding:14px 16px;display:flex;flex-direction:column;gap:2px;min-width:0}
.stat .tl{color:var(--ink-soft);font-size:13px;font-weight:600}
.stat .tn{font-size:22px;font-weight:700;letter-spacing:-.01em;line-height:1.2;
  font-variant-numeric:tabular-nums}
.stat .ts{margin-top:auto;padding-top:6px;color:var(--muted);font-size:12px}
/* The status chips, restated for this parent. `.chip.chip-ok` is defined
   globally at the top of the file, and `.stat .ts` has equal specificity and
   comes LATER -- so without these the week-over-week chip kept its green
   background and lost its green text to the muted default. The old `.tile`
   rules carried the same pair for the same reason. */
.stat .ts.chip-ok{background:var(--success-bg);color:var(--success);
  border-radius:999px;padding:2px 8px;font-weight:600}
.stat .ts.chip-warn{background:var(--warn-bg);color:var(--warn);
  border-radius:999px;padding:2px 8px;font-weight:600}
/* The lead tile was twice the width with a 38px figure until 2026-09-10;
   the owner wants it the same as the three beside it, so .stat-lead is only
   a name now. */
/* THE LABELS ALL MATCH. Owner 2026-09-09, same screenshot: "'listings
   created' is the largest font out of the others". It was 14px against the
   other three at 13px -- so the tile led twice, once with its number and again
   with its caption, and the caption is not the thing worth leading with. The
   NUMBER still leads, which is the whole point of the lead tile. */
.stat-lead .tl{font-size:13px}
@media (max-width:700px){.stat-lead{grid-column:span 1}}

.sec-head{display:flex;align-items:center;margin-bottom:10px}
.sec-head h2{font-size:19px}
.sec-head a{margin-left:auto;font-weight:600;font-size:14px;cursor:pointer}
.dash-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px 12px;margin-bottom:24px}
.pcard{position:relative;background:#fff;border:1px solid var(--line);border-radius:6px;padding:7px 7px 13px;
  display:flex;flex-direction:column;gap:8px}
.pcard>*:not(.pcov):not(.pdel){margin-left:6px;margin-right:6px}
.pcard .pcov{aspect-ratio:210/117;border-radius:6px;background:var(--bg) center/contain no-repeat;
  margin-bottom:6px}
/* Titles wrap in full. min-height reserves two lines so cards in a row still
   align, but the title is free to run to three or four — an eBay title is up to
   80 characters and knowing which listing a draft is matters more than a tidy
   card height. word-break lets a long part number break rather than overflow. */
.pcard .pt{font-weight:600;font-size:14px;line-height:18px;min-height:36px;
  white-space:normal;overflow:visible;word-break:break-word}
.pcard .pp{font-weight:700;font-size:16px}
/* The title min-height reserves two lines so a row of cards lines up, but an
   eBay title runs to 80 characters and often takes three -- and then the card
   below it kept its buttons where a two-line title left them, so the action
   rows stepped up and down across the row. The card is already a flex column
   and the grid already stretches them to equal height, so the actions just
   need to sit at the bottom of it. Titles stay uncapped. */
.pcard .pa{display:flex;gap:8px;align-items:center;margin-top:auto}
.pcard .pa .btn-blue,.pcard .pa .btn-soft[data-revise]{flex:1;padding:7px 14px;font-size:14px}
/* Readiness and activity used to be two columns, 684fr against 448fr, and the
   rail simply ran out: a three-row static panel beside a seven-row list left
   about 325px of nothing. They are full-width blocks in reading order now,
   readiness first because it is the only thing here that can stop the work. */
.dash-conn-card,.dash-activity-card{margin-bottom:24px}
/* Three states across the strip rather than stacked down a rail. */
#dash-conn{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));
  gap:12px}
#dash-conn .ready-row{margin-bottom:0}
.dash-activity-card,.dash-conn-card{padding:17px 25px 12px}
.dash-activity-card h2,.dash-conn-card h2{font-size:19px;margin-bottom:14px}
/* Seven bordered rows inside a bordered card is seven boxes drawn inside an
   eighth, and it is the same multiplication the "View on eBay" link was quieted
   for: the least important thing on the dashboard was drawn the most times.
   The rows are separated by a hairline instead, which is what divides a list
   from its neighbours everywhere else in the app. */
.act-row{display:flex;align-items:center;gap:14px;
  border-bottom:1px solid var(--line);padding:12px 2px;min-height:66px}
.act-row:last-child{border-bottom:0;padding-bottom:6px}
.act-row .act-txt{flex:1;min-width:0}
/* Recent Activity titles wrap instead of ending in an ellipsis. These are eBay
   titles up to 80 characters, and every one of them ended "...Full LED ..." —
   which is where the side and the fitment live, the two things you actually need
   to tell two rows apart. The row has a min-height, not a fixed one, so it grows. */
.act-row .t{font-weight:600;font-size:14px;white-space:normal;
  overflow-wrap:anywhere;line-height:1.35}
.act-row .muted{font-size:13px}
/* Recent Activity repeats one row seven times, so whatever this link looks like
   gets multiplied by seven. As a solid blue button it was the loudest thing on
   the dashboard — seven primary buttons for the least important action on the
   page, out-shouting "List a headlight". Quiet by default, blue on hover, so the
   affordance is still obvious once you go looking for it. The ↗ matches the
   "View on eBay ↗" the publish status already prints. */
/* Empty state: the first screen a seller with no data lands on. It spans the
   whole grid so it centres on the page rather than sitting in column one. */
.empty-state{grid-column:1/-1;display:flex;flex-direction:column;align-items:center;
  text-align:center;gap:10px;padding:72px 24px 84px}
.empty-state .es-ic{width:56px;height:56px;border-radius:var(--r-lg);background:var(--tint);
  display:inline-flex;align-items:center;justify-content:center;margin-bottom:4px}
.empty-state .es-ic svg{width:26px;height:26px;stroke:var(--accent)}
.empty-state h2{font-size:19px;font-weight:700;letter-spacing:-.012em}
.empty-state p{color:var(--ink-soft);font-size:14px;line-height:1.55;max-width:42ch;margin:0}
.empty-state .btn-blue{margin-top:8px}

.act-row .act-view{flex:none;padding:7px 14px;font-size:13px;border-radius:4px;
  color:var(--ink-soft);transition:color .14s,border-color .14s,background .14s}
.act-row .act-view:hover{color:var(--accent);border-color:var(--accent);
  background:var(--accent-soft)}
/* READY IS QUIET, NOT ABSENT. Owner ruling 2026-08-29 keeps this card first
   and full width, because it is the one thing on the page that can stop the
   work. That ruling is about its PLACE. Its volume is a separate question, and
   three rows of green ticks on an account with nothing to do is furniture at
   the top of every visit.

   So when nothing needs action the rows fold away and the head carries a
   one-line summary. Nothing is hidden that was not already answered: the
   summary states the same fact the ticks did. The moment one thing needs
   doing, the card is back at full height with only that row's action to find.

   Collapsed by max-height rather than display:none so it animates, and so the
   rows stay in the accessibility tree for anyone reading the page linearly. */
.dash-conn-card.is-ready #dash-conn{max-height:0;overflow:hidden;opacity:0;
  margin:0;transition:max-height .24s cubic-bezier(.2,.8,.2,1),opacity .18s}
.dash-conn-card #dash-conn{max-height:400px;opacity:1;
  transition:max-height .24s cubic-bezier(.2,.8,.2,1),opacity .18s}
.dash-conn-card.is-ready .sec-head{margin-bottom:0}
.ready-sum{margin-left:auto;display:inline-flex;align-items:center;gap:6px;
  font-size:14px;font-weight:600;color:var(--ink)}
.ready-sum::before{content:"";width:8px;height:8px;border-radius:0;
  background:var(--accent)}
@media (prefers-reduced-motion:reduce){
  .dash-conn-card #dash-conn,.dash-conn-card.is-ready #dash-conn{transition:none}}

.ready-row{display:flex;align-items:center;gap:11px;border:1px solid var(--line);
  border-radius:6px;padding:11px 14px;margin-bottom:13px;min-height:46px}
.ready-row .ready-ic{width:24px;height:24px;border-radius:0;display:inline-flex;
  align-items:center;justify-content:center;font-size:12px;font-weight:700;flex:none}
.ready-row .ready-ic.ok{background:var(--accent);color:#fff}
.ready-row .ready-ic.warn,.ready-row .ready-ic.off{background:var(--warn-bg);color:var(--warn)}
/* The badge holds a DRAWN tick now, not a typeface one. A font's check mark is
   curved by the typeface and no stroke property can square it off, which is
   what made this the curviest mark on the site. Sized here because the badge
   is 24px and a bare viewBox svg would fill it. */
.ready-row .ready-ic svg{width:14px;height:14px;display:block}
.ready-row .ready-lbl{flex:1;font-weight:600;font-size:14px}
.ready-row a{font-weight:600;font-size:14px;cursor:pointer}

/* ============ new listing ============ */
/* Equal halves (owner 2026-08-15): photos and details always split 50/50.
   min-width:0 keeps a long filmstrip scrolling INSIDE its half instead of
   widening the column and shifting the page on upload. */
.nl-grid{display:grid;grid-template-columns:1fr 1fr;gap:24px;align-items:start;padding-bottom:180px}
.nl-grid>*{min-width:0}
/* The photo column is 629px against a 1191px form, so the left half of this
   page sat empty for ~560px while the seller scrolled the right half. Sticking
   it fills that space with the thing they most want to keep looking at: you
   describe the condition and check the specifics against the photo, and
   previously the photo had scrolled away by then.
   Only above 1050px — below that the grid is one column and a sticky photo
   block would pin itself over the form it is meant to accompany. */
@media (min-width:1051px){
  .nl-photos{position:sticky;top:24px;
    /* never taller than the viewport, or the bottom of it becomes unreachable */
    max-height:calc(100vh - 48px);overflow:auto}
  /* AND THE STATUS BAR STICKS WITH IT, at the same 24px. Owner 2026-09-06,
     twice: "the thumbnail page is still not aligned with the button logo
     status blue bar", then "The whole thumbnail page itself is not aligning
     up. The edges aren't aligning up."

     The edges DO align -- at scroll zero, which is the only place I kept
     checking. The photo card is sticky and the right column was not, so the
     moment the seller scrolled the card pinned 24px below the top of the
     window while the bar rode up the page with the form. They came apart
     immediately and stayed apart for the whole time anyone is actually
     looking at them, which is why two structural fixes did not touch it.

     Sticking the bar at the same offset makes them agree at EVERY scroll
     position rather than one. It also keeps the thing that says what the app
     is doing on screen while the seller works down the form, which is the
     job that bar was added for.

     z-index so the form scrolls under it rather than through it; it has its
     own opaque background already.

     ONLY this bar. #photo-review spans the whole grid above both columns, so
     pinning it at the same 24px would park it on top of this one -- two
     stickies at one offset occupy the same line. It scrolls away normally,
     which is right: it is a note about work happening elsewhere, not the
     status of what the seller is waiting for. */
  /* NOT STICKY, AND THAT IS THE THIRD RULING ON THIS BAR.

     Owner 2026-09-07, third time: "when I scroll down, the blue bar
     follows me on the screen and doesn't stay in place." 4377e7e made it
     sticky at 24px so it stayed level with the photo card while scrolling,
     which answered an earlier complaint about the two columns diverging.
     11f8ba2 then made only the SETTLED state static, on the reading that a
     finished sentence should stay put and live work should follow. He has
     now said it plainly about the running state too.

     So the bar never follows. What the earlier fix was actually for is
     preserved: at rest sticky and static paint in the same place, so the
     card and the bar still start on one line, which is the alignment he
     asked for and the only part of it he could see. The photo card keeps
     its own sticky -- he has not complained about the photos staying
     reachable, and that is the one thing on this page worth keeping in
     view while the form is filled in.

     If a fourth ruling arrives here, read it as being about the CARD
     rather than the bar. */
  #id-working{position:static;z-index:5}
  /* The `settled` half of this used to be a separate rule: the bar was
     sticky while working and static once it had a result, on the reading
     that live work may follow you and a finished sentence should not. The
     owner then said the running state should not follow either, so the
     distinction collapsed and the rule above covers both. Kept as a note
     rather than deleted because `settled` still means what it meant, and
     the next person to want a sticky status bar should know it was tried
     twice. */
}
.nl-photos{padding:25px}
.nl-photos h2,.nl-details h2{font-size:19px;margin-bottom:16px}
.nl-photos h2{margin-bottom:10px}
.nl-main{aspect-ratio:398/244;border-radius:6px;background:var(--bg) center/contain no-repeat;cursor:pointer;
  position:relative}
.nl-main.empty{background-image:none}
/* The thumbnail shimmers with the title field while the photo tools run
   (owner 2026-09-01: "the same pace and exact time as on the eBay title").

   Same keyframes, same 2.4s, same linear timing, and both classes are applied
   in the SAME titleBusy() call -- CSS animations started in one frame stay in
   phase, so the two bands cross their boxes together rather than drifting
   apart over a 40s upload. Sharing tb-skeleton is what guarantees that: two
   copies of the same numbers would only look synchronised until one was
   retuned.

   Scoped to .empty (owner 2026-09-01: "the shimmer should disappear when
   the headlight appears in the thumbnail"). renderPhotos toggles that class
   off in the SAME statement that sets the background image, so the shimmer
   stops on the frame the photo lands -- not when the pipeline ends, which
   is tens of seconds later and left it running over a visible headlight.
   Driven by the flag that draws the photo rather than a second JS call,
   because the two cannot then fall out of step. */
.nl-main.empty.is-working{position:relative;overflow:hidden}
.nl-main.empty.is-working::after{content:"";position:absolute;top:0;bottom:0;left:0;width:200%;
  background-image:linear-gradient(90deg,
    rgba(43,108,246,0) 0%, rgba(43,108,246,.13) 25%, rgba(43,108,246,0) 50%,
    rgba(43,108,246,0) 50%, rgba(43,108,246,.13) 75%, rgba(43,108,246,0) 100%);
  animation:tb-skeleton 2.4s linear infinite;will-change:transform}
@media (prefers-reduced-motion:reduce){
  .nl-main.empty.is-working::after{animation:none;transform:none;width:100%;
    background-image:none;background:rgba(43,108,246,.07)}
}
.nl-ph-head{display:flex;align-items:center;gap:10px;margin:10px 0}
.nl-ph-head h3{font-size:19px;flex:1}
.nl-ph-head #photo-tools{display:inline-flex;gap:8px}
.nl-ph-head .btn-soft{padding:6px 12px;font-size:13px;background:var(--accent-soft);border:0;color:var(--ink-soft)}
.nl-ph-head .btn-soft:hover{background:var(--tint-line)}
/* padding-bottom pays for the scrollbar the rule below draws; at 6px the
   8px bar sat on the thumbnails' bottom edge. */
.nl-strip{display:flex;gap:11px;overflow-x:auto;padding-bottom:10px}
.nl-strip .thumb{width:91px;height:91px;border-radius:6px;background:var(--bg) center/contain no-repeat;
  flex:none;position:relative;border:1.5px solid transparent;cursor:pointer}
.nl-strip .thumb.sel{border-color:var(--accent);background-color:#fff}
.nl-strip .thumb .x{position:absolute;top:4px;right:4px;width:18px;height:18px;border-radius:50%;
  background:#fff;border:1px solid var(--line);display:flex;align-items:center;justify-content:center}
.nl-strip .thumb .x svg{width:9px;height:9px;stroke:var(--ink-soft)}
.nl-strip .thumb .num,.nl-strip .thumb .cover{display:none}
/* the photo's own button, covering its tile (2026-09-11: no button inside a button) */
.thumb .thumb-hit{position:absolute;inset:0;width:100%;height:100%;margin:0;padding:0;
  border:0;border-radius:inherit;background:transparent;cursor:pointer}
.thumb .thumb-hit:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
.nl-strip .dash-tile{width:91px;height:91px;border-radius:6px;border:1.5px dashed var(--line-strong);
  display:flex;align-items:center;justify-content:center;flex:none;cursor:pointer}
.nl-strip .dash-tile svg{width:18px;height:18px;stroke:var(--muted)}
.nl-drop{border:1.5px dashed var(--line-strong);border-radius:6px;padding:20px;margin-top:10px;
  display:flex;flex-direction:column;align-items:center;gap:4px;text-align:center;cursor:pointer}
.nl-drop svg{width:22px;height:22px;stroke:var(--accent)}
.nl-drop .dz-t{color:var(--accent);font-weight:600;font-size:14px}
.nl-drop .muted{font-size:13px}
/* Nothing new goes in while the model is reading what is already there
   (owner 2026-09-02). Named one by one rather than caught by a shared class,
   because these five are different elements in four different containers and
   the only thing they have in common is that clicking one opens the file
   picker. uploadFiles refuses regardless -- this exists so the picker never
   opens in the first place. */
.uploads-locked #dropzone,
.uploads-locked .dash-tile,
.uploads-locked #pm-add,
.uploads-locked #btn-reidentify,
.uploads-locked #thumb-main.empty{pointer-events:none;opacity:.5;
  cursor:default}
/* Publish failures: eBay's own error, kept on screen (a toast scrolls away
   before the seller can read an error id). Owner ask 2026-08-15. */
.list-error{display:flex;flex-direction:column;gap:5px;margin:14px 0;padding:12px 16px;
  background:#FDECEC;border:1px solid #F2C2C2;border-radius:6px;font-size:13px;color:#7A2020}
.list-error .le-tag{font-weight:700;font-size:11px;letter-spacing:.05em;text-transform:uppercase;color:#C0392B}
.list-error .le-text{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;
  word-break:break-word}
.list-error .le-hint{color:#8A5A5A;font-size:13px}
.list-error .le-rows{display:flex;flex-direction:column;gap:6px}
.list-error .le-row b{font-weight:600}

/* Log in, beside the primary action in the marketing nav. Quiet on purpose:
   a returning seller knows where it is, and a visitor should be looking at
   See pricing. */
.mk-login{font-size:14px;font-weight:600;color:var(--ink-soft);
  text-decoration:none;white-space:nowrap}
.mk-login:hover{color:var(--ink);text-decoration:underline}
/* The balance on New Listing. Quiet: it is a fact to have, not a thing to act
   on -- Billing is where money is spent. */
.nl-credit{margin:0 0 10px;font-size:12px;color:var(--muted);
  font-variant-numeric:tabular-nums}
/* Gate rulings. The form is a card of eight controls of very different widths
   -- a two character state next to a sentence-long note -- so a plain 3-column
   grid left ragged gaps and the button sat flush against the last row. Sized
   to the content, and the card spaces its own rows rather than inheriting
   whatever margins its children brought. */
#view-gate .card{display:flex;flex-direction:column;align-items:flex-start;
  gap:14px;max-width:920px}
#view-gate .billing-msg{max-width:920px}
#view-gate .sec-head{margin-top:26px}
#view-gate #gate-list .card{gap:6px;padding:16px 18px}
#view-gate #gate-list .pt{font-weight:700}
#view-gate #gate-list .btn-soft{margin-top:8px}
/* Billing's own status line. The reason it exists: a top-up that failed used
   to report through a banner living inside the New Listing view, so the seller
   saw nothing at all. */
/* THE 640px CAP IS GONE, AND THIS IS THE NOTE SAYING WHY.

   It was right for what the page was: "a balance, two rates and one button"
   left to the full width put one line of text across the screen with a card
   of dead space under it (owner 2026-09-02). The cap was the fix.

   The page is two columns now (owner 2026-09-08: "why is everything
   vertically placed"), so the measure is the column, not the viewport, and a
   640px cap on top of a 320px rail would leave a gap inside every card in it.
   The reason the cap existed is answered by the layout instead.

   `align-items:flex-start` went with it for the same reason: it shrink-wrapped
   every child, which is invisible while a card holds one paragraph and wrong
   the moment it holds a heading with an action pushed to the right.

   .billing-msg keeps a measure of its own -- it is a banner of running text
   above the columns, and running text still has a comfortable line length. */
#view-billing .billing-msg{max-width:640px}
#view-billing .card{display:flex;flex-direction:column;
  gap:10px;padding:20px 22px}
/* .sec-head sets its own h2. This existed to shrink a bare h2 in a card and
   would now be a second heading system arguing with the shared one. */
#view-billing .sec-head{margin-bottom:2px}
#view-billing .card p{margin:0;max-width:58ch;line-height:1.6}
/* An emptied paragraph is still a flex child and still takes a gap, which is
   what left a comped account looking at a card of nothing. */
#view-billing .card p:empty{display:none}
#view-billing .card .btn-pill{margin-top:4px}
/* Four amounts, the smallest as the primary. $5 stays first because it is the
   minimum and the one the explainer names (owner 2026-09-03). */
.topup-row{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin-top:4px}
.topup-row .btn-soft{min-height:44px}
.topup-custom{display:inline-flex;align-items:center;gap:8px}
.topup-custom[hidden]{display:none}
.topup-cur{color:var(--muted);font-weight:600}
/* Wide enough for "1000" and no wider: a full-width field here would read as
   the primary way to pay rather than the exception. */
#topup-amount{width:96px;min-height:44px;font-variant-numeric:tabular-nums}
.billing-msg{margin:0 0 14px;padding:10px 14px;border-radius:6px;font-size:13px;
  background:var(--tint);border:1px solid var(--tint-line);color:var(--ink-soft)}
.billing-msg.err{background:#FDECEC;border-color:#F3C0C0;color:#8B1F1F}
.billing-msg.warn{background:#FFF6E6;border-color:#F0DCA8;color:#7A5A00}
.btn-blue.is-busy,.rewrite-chip.is-busy,.btn-pill.is-busy{opacity:.85;cursor:progress}
.rewrite-chip.is-busy .spinner,.btn-blue.is-busy .spinner,.btn-pill.is-busy .spinner,
.btn-danger.is-busy .spinner{
  border-color:rgba(255,255,255,.45);border-top-color:#fff}

.id-banner{display:flex;align-items:center;gap:8px;background:#fff;
  border:1px solid var(--line-strong);border-radius:0;padding:16px 15px;margin-bottom:12px}
.id-banner img,.id-banner .idb-ic{width:42px;height:42px;background:var(--btn);border-radius:0;padding:9px;flex:none;display:block}
.id-banner .idb-ic svg{width:100%;height:100%;display:block}
.id-banner .idb-txt{display:flex;flex-direction:column;flex:1;min-width:0}
/* Mono, like every value the page fills in (owner 2026-09-11, board D). */
.id-banner .idb-txt b{font:400 12px/1.4 var(--mono);color:var(--ink);overflow-wrap:anywhere}
.id-banner .idb-txt .muted{font:400 11px/1.4 var(--mono)}
.id-banner .idb-ic svg{animation:tick-pop .3s cubic-bezier(.2,1.5,.4,1) both}
@media (prefers-reduced-motion:reduce){.id-banner .idb-ic svg{animation:none}}
.id-banner .idb-conf{color:var(--accent);font-weight:600;font-size:13px}
.id-banner .idb-conf.warn{color:var(--warn)}

/* Identify's working state (owner 2026-08-31, final). The chip stays exactly
   as the result banner has it -- 42px, white mark on blue -- so the bar does
   not change shape when the identity lands. Only the copy differs.

   CLIPPED, and that is the load-bearing line. The mark is white; anything that
   leaves the blue square sits on --accent-soft and is invisible, which is what
   made the first version unrecognisable -- pieces appeared at the chip's edge
   instead of flying in. Clipping turns that into pieces entering from the
   chip's own edges, which reads as deliberate at this size.

   All three share the 2.15s clock so they land together; the 30-72% hold is
   what stops the loop flickering. */
.id-working{align-items:center}
/* NO overflow:hidden, and that is the fix rather than an omission (owner
   2026-09-03: "the loading animation is wobbly like an oval"). The three paths
   assemble from OUTSIDE the frame, so something must clip them -- but clipping
   them here meant clipping against a 6px-rounded 42px tile, and a shape sliding
   past a rounded corner bulges as it passes. The <svg> already clips at its own
   viewBox by default, squarely, which is what the animation was drawn against.
   The tile keeps its chrome (square since 2026-09-10); the animation stops
   being cut by it. */
/* THE RING (2026-09-10). The three tiles that assembled here were the old
   logo; the wordmark replaced it and the owner wants no trace, in the app as
   on the public pages. The chip stays exactly as it was -- 42px, the blue,
   the same box as the result banner's icon -- and a white arc turns on it.
   The same ring the public demo shows while it identifies. */
.idw-mark{width:42px;height:42px;background:var(--btn);border-radius:0;
  padding:9px;flex:none;display:block;box-sizing:border-box}
.idw-mark svg{width:100%;height:100%;display:block;animation:idwSpin 1.1s linear infinite}
.idw-mark circle{fill:none;stroke:#fff;stroke-width:5;stroke-linecap:butt;stroke-dasharray:78 29}
@keyframes idwSpin{to{transform:rotate(360deg)}}
/* The dots are their own element so the word does not jitter with them. */
.idw-dots{display:inline-block;width:1.2em;text-align:left}
.idw-dots::after{content:".";animation:idwDots 1.4s steps(1,end) infinite}
@keyframes idwDots{0%{content:"."}33%{content:".."}66%{content:"..."}}
@keyframes idwBreathe{0%,100%{opacity:1}50%{opacity:.62}}
/* Reduced motion: no movement, but still the logo and still a sign of life. */
@media (prefers-reduced-motion:reduce){
  .idw-mark svg{animation:none}
  /* The dots are movement too. Settled on three, so the bar still reads as
     an ongoing action rather than a finished one. */
  .idw-dots::after{animation:none;content:"..."}
  .idw-mark{animation:idwBreathe 2.9s ease-in-out infinite}
}
.nl-details{padding:25px}
.fhead{display:flex;align-items:center;gap:10px}
.fhead label{font-weight:600;font-size:14px;flex:1}
.nl-sub{font-size:16px;margin:16px 0 14px}
.nl-note{display:inline-flex;align-items:center;gap:6px;font-size:13px}
.nl-note img{width:12px;height:12px}
.nl-price-row{display:flex;gap:6px}
.nl-price{display:flex;align-items:center;gap:6px}
.nl-price input{border:0;outline:0;background:none;font:inherit;font-weight:700;
  color:var(--accent);width:100%}
.nl-price span{color:var(--muted);font-weight:600}
.nl-quick{margin:10px 0}
.cond-quick{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:8px}
.cond-actions{display:flex;gap:8px}
.spec-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px 24px;margin-bottom:12px}
.title-choices{display:flex;flex-wrap:wrap;gap:8px}
.nl-bar{position:fixed;left:auto;right:24px;bottom:18px;background:#fff;border:0;flex-wrap:nowrap;
  border-radius:6px;box-shadow:var(--shadow-lg);display:flex;align-items:center;gap:10px;
  padding:14px;z-index:30}
/* The status span is a permanent child that is usually empty, and an empty
   flex item still takes its share of the 10px gap -- which pushed the first
   button to 25px from the left while the last sat 14px from the right. */
.nl-bar .status:empty{display:none}
/* The bar holds New and Next and nothing else (owner 2026-09-03: "Cut the
   bottom bar on the listing page to only fit the 'new' and next buttons").
   The summary that used to fill it repeated the title and description from the
   fields two inches above it, and it was what forced the bar to span the whole
   column. margin-left:auto on the first control is what replaces flex:1 on a
   block that no longer exists. */
.nl-bar #btn-new{margin-left:auto}
.corr-panel{border:1px solid var(--line);border-radius:6px;background:#fff;margin:12px 0;padding:0 16px}
.corr-panel summary{padding:12px 0;font-weight:600;cursor:pointer;color:var(--ink-soft)}
/* A preferences card that opens. Closed it is one row: the name on the left and
   its current setting on the right, so the page stays scannable without hiding
   what the setting actually is. */
/* These two cards are driven entirely by the switch in their header: it turns
   the feature on and reveals its settings, so an off card is a title, a line of
   explanation and a switch. The status text and the Set up button that used to
   sit here were a third and fourth control for the same on/off decision
   (owner 2026-08-20). */
/* Where a confirmed part number goes. Nested under its switch because the
   choices mean nothing until the reader is on. */
/* The photo opt-in on a revise. Sits with the action, not in the photo strip,
   because it is a decision about THIS apply rather than a property of the
   photos. */
.rv-photos{display:inline-flex;align-items:center;gap:9px;font-size:13px;
  font-weight:600;color:var(--ink-soft);cursor:pointer;white-space:nowrap;
  border:1px solid var(--line);border-radius:4px;padding:7px 13px;background:#fff}
/* The second line is the whole point: a checkbox label says what ticking does,
   never what leaving it alone does, and that is the half a seller worries
   about with their live photos. */
.rv-photos span{display:flex;flex-direction:column;line-height:1.25}
.rv-photos small{font-weight:500;font-size:12px;color:var(--muted)}
.rv-photos:has(input:checked) small{color:var(--accent);opacity:.85}
.rv-photos:hover{border-color:var(--line-strong)}
.rv-photos input{accent-color:var(--accent);width:15px;height:15px;cursor:pointer}
.rv-photos:has(input:checked){border-color:var(--accent);color:var(--accent);
  background:var(--accent-soft)}

.pn-where{border-top:1px solid var(--line);padding:14px 0 2px;margin-top:2px}
/* Inside Photo tools these blocks are one switch's own detail, not the next
   setting along. A full-width rule across the top is exactly how this card
   separates one setting from the next, so the sub-options read as peers of the
   switches they belong to. The rule turns on its side and they indent under
   their parent. (The seats code block keeps the separator: it IS a new thing.) */
#pt-stock,#pt-country-where,#pt-where{
  border-top:0;border-left:2px solid var(--line);
  padding:4px 0 4px 16px;margin:2px 0 10px 12px}
.pn-where .card-hint{margin-bottom:10px}
.pn-opt{display:flex;gap:10px;align-items:flex-start;padding:7px 0;
  font-size:14px;color:var(--ink-soft);line-height:1.5;cursor:pointer}
.pn-opt input{margin-top:3px;accent-color:var(--accent);width:15px;height:15px;
  flex:none;cursor:pointer}
.pn-opt b{color:var(--ink);font-weight:600}

.pk-card .card-head-row{margin-bottom:6px}
.pk-card .card-head-row h2{flex:1}
.pk-card>.card-hint{display:block;max-width:76ch}
.pk-body{margin-top:16px}
.corr-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px;padding-bottom:12px}
.corr-grid label{display:flex;flex-direction:column;gap:4px;font-size:13px;color:var(--ink-soft)}
.corr-grid input,.corr-grid select{border:1px solid var(--line);border-radius:4px;padding:7px 10px;font:inherit}
/* free text needs the full row; the paired dropdowns above it do not */
.corr-grid .corr-wide{grid-column:1/-1}
/* Owner 2026-08-30: space the feature list off the actions, and push the
   buttons right. The status message keeps the left edge so a "saved" or an
   error reads where the eye already is, and margin-left:auto on the first
   button carries both it and Submit as anchor over. */
.corr-actions{display:flex;gap:10px;align-items:center;
  padding:16px 0 12px;flex-wrap:wrap}
.corr-actions #idc-apply{margin-left:auto}
.corr-math{font-size:13px;color:var(--ink-soft);padding-bottom:8px}
.corr-bands{display:flex;gap:10px;padding-bottom:8px}
.pvmodal{position:fixed;inset:0;background:rgba(16,17,21,.5);display:flex;align-items:center;
  justify-content:center;z-index:50}
.pvpanel{background:#fff;border-radius:6px;max-width:560px;width:92%;max-height:88vh;
  overflow:auto;position:relative;padding:24px}
.pvclose{position:absolute;top:12px;right:14px;font-size:16px}
@media (max-width:1250px){.nl-bar .status{display:none}}
@media (max-width:1050px){.nl-grid{grid-template-columns:1fr}}

/* ---- listing-view details the JS renders into ---- */
.seg-pill .seg.on{background:var(--accent);color:#fff}
/* [data-icon] spans are filled by paintIcons(); without a size the raw SVG fills the page */
[data-icon]{display:inline-flex;align-items:center;justify-content:center}
[data-icon] svg{width:18px;height:18px}
/* AT THE BOTTOM, NOT FLOATING ABOVE IT (owner 2026-09-11: "it should be right
   at the bottom of the screen and not awkwardly have a large gap under it").
   The bar it used to clear is right-aligned and the toast is centred, so they
   only ever meet on a narrow window: the toast's right edge is 50vw + 280 and
   the bar's left edge is 100vw - 218, which cross below ~996px. Above that the
   toast sits flush; below it, and only there, it still lifts clear of the bar
   rather than covering the button the seller just pressed. */
@media (max-width:1000px){
  body:has(#view-listing:not([hidden])) .toast{bottom:92px}
}
#title-count.over{color:var(--danger);font-weight:700}
/* Title-bar busy state (owner 2026-08-31, revised after living with it).

   The field itself carries NO text. It is a skeleton: an empty bar with a
   light band travelling across the whole of it, which is what a box about to
   be filled in looks like. The step name lives in the status line ABOVE the
   field, where the character count and the AI messages already are, and
   shimmers in step with it.

   That split is the point. One thing says "this box is being filled" and one
   thing says "here is what is running"; the earlier version made the field do
   both and the label had to fight the box for room.

   An overlay rather than text in the input: an <input> cannot contain animated
   markup, and putting a placeholder string in .value risks it being SAVED as
   the title. The real input is emptied and hidden behind this. */
.title-busy-wrap{position:relative;display:block}
.title-busy{position:absolute;inset:0;border:1px solid var(--line);border-radius:6px;
  background:var(--surface,#fff);overflow:hidden;pointer-events:none}
/* A CIRCULAR sweep (owner 2026-08-31: "smoother (circular)").
   The band used to sweep across and snap back: background-position with
   no-repeat leaves dead time at both ends, and ease-in-out accelerated and
   braked inside every pass, so one loop never matched the next.
   Now the layer is TWICE the field's width and carries two identical bands,
   one at 25% and one at 75%. Translating it by exactly -50% moves it by one
   whole band period, so the last frame is pixel-identical to the first and the
   loop has no seam. transform rather than background-position because it is
   composited, and linear rather than eased because a constant speed is what
   makes it read as circular rather than as a repeated gesture. */
.title-busy-wrap.fld-busy-wrap{display:block}
.fld-busy-wrap{position:relative}
.fld-busy{position:absolute;inset:0;border:1px solid var(--line);background:var(--surface,#fff);
  overflow:hidden;pointer-events:none;z-index:2}
.title-busy::after,.fld-busy::after{content:"";position:absolute;top:0;bottom:0;left:0;width:200%;
  background-image:linear-gradient(90deg,
    rgba(43,108,246,0) 0%, rgba(43,108,246,.13) 25%, rgba(43,108,246,0) 50%,
    rgba(43,108,246,0) 50%, rgba(43,108,246,.13) 75%, rgba(43,108,246,0) 100%);
  animation:tb-skeleton 2.4s linear infinite;will-change:transform}
/* LEFT TO RIGHT (owner 2026-08-31). Translating the layer NEGATIVE drags it
   left and carried the band the wrong way. Starting shifted left and ending
   at rest moves it right, and the loop is still seamless: the band sitting
   mid-field on the last frame is the one that starts there on the first. */
@keyframes tb-skeleton{from{transform:translateX(-50%)}to{transform:translateX(0)}}

/* The banner above the title is the ONE status (owner 2026-08-31). While a
   task runs the mark assembles; `settled` is the same bar with the work
   over -- animation stopped, no trailing dots -- so a result lands exactly
   where the work was reported instead of somewhere else on the page. */
.id-working.settled .idw-mark svg{animation:none}
.id-working.settled .idw-dots{display:none}
.id-working.err .idb-txt b{color:var(--danger)}
.id-working.ok .idb-txt b{color:var(--ink)}
@keyframes tb-shimmer{from{background-position:-120% 0}to{background-position:120% 0}}

/* Locked in CLAUDE.md, and a moving gradient is exactly where it is forgotten.
   The bar stays as a still tint so the box still reads as occupied, and the
   status keeps its words -- the words were always the information. */
@media (prefers-reduced-motion:reduce){
  .title-busy::after,.fld-busy::after{animation:none;transform:none;width:100%;
    background-image:none;background:rgba(43,108,246,.07)}
}
.status{font-size:13px;color:var(--muted)}
.status a{font-weight:600}
#btn-next{background:var(--btn)}
#btn-next:hover{background:var(--btn-hover)}
/* No disc behind the arrow (owner 2026-09-11: "remove the white highlight on
   the arrow on the new listing page 'next'"): the arrow sits on the button's
   own blue, disabled or not. */
#btn-next .ic{background:none}
.nl-bar .btn-pill:disabled{background:#EDEFF8;color:var(--muted);box-shadow:none;transform:none;cursor:default}
.nl-bar .btn-pill:disabled .ic{background:none}
.corr-panel .corr-actions button{border:1px solid var(--line);border-radius:4px;padding:7px 12px;
  font-size:13px;font-weight:600;background:#fff;color:var(--ink-soft)}
/* Submit anchor is blue (owner 2026-08-17, retargeted 2026-09-01 when the
   ids it named -- idc-save, sev-save -- were both gone and the rule had
   been matching nothing) — the one app button that is,
   because teaching the identifier is the thing the marketing page is about and
   the Anchor Submitted chip beside it is blue for the same reason. Apply to
   listing stays plain, so the two do not compete. */
.corr-panel .corr-actions #idc-anchor{
  background:var(--cta);border-color:var(--cta);color:#fff}
.corr-panel .corr-actions #idc-anchor:hover{
  background:var(--cta-hover);border-color:var(--cta-hover)}
.corr-panel .ok{color:var(--ink);display:inline-flex;align-items:center;gap:8px}
.corr-panel .err{color:var(--danger)}
.corr-bands label{display:flex;flex-direction:column;gap:4px;font-size:13px;color:var(--ink-soft)}
.corr-bands input,.corr-panel #sev-note{border:1px solid var(--line);border-radius:4px;
  padding:7px 10px;font:inherit}
.thumb.dragging{opacity:.4;transform:scale(.93)}
.thumb.drop-before,.pm-tile.drop-before{box-shadow:-4px 0 0 0 var(--accent)}
.thumb.drop-after,.pm-tile.drop-after{box-shadow:4px 0 0 0 var(--accent)}
/* The photo tools, working. Owner 2026-09-05: "apply a light grey tint over
   the photo to show the user the photo tool in action."

   BLUE, not grey. Owner 2026-09-05: "change the tint to the current blue tint
   we use when the drop in is idle (on the demo)." That is --accent (#2563EB),
   the same hue --tint / --accent-soft is a pale wash of; laid over a photo it
   needs to be the saturated value at low alpha rather than the pale one at
   full, or it reads as fog instead of blue.

   An overlay rather than opacity or a filter: the photo underneath keeps its
   own colour, so the tile still reads as that photo rather than a washed-out
   one.

   It does share a hue with .sel, which is the blue border. They co-occur --
   the selected photo can be one the tools are working on -- so the tint is
   kept well below the border's weight: a wash reads as a state of the picture,
   an edge reads as a choice about it.

   ::after is the tile's last child, so it paints over the remove button, which
   is positioned with no z-index of its own. Lifted only while working, rather
   than by changing the base rule: the seller must still be able to see and hit
   Remove on a photo he has changed his mind about mid-pass. pointer-events
   are off on both layers, so the click was never actually blocked -- this is
   about being able to SEE the control he is aiming at.

   The sheen crossing it is the same idea as the title bar's: a still tint says
   "disabled", a moving one says "working". prefers-reduced-motion keeps the
   tint and drops the movement -- the tint is the information, the sweep is
   only the reassurance.

   rgba(37,99,235,...) is how this file writes a translucent accent everywhere
   else -- the identify spinner's ring at .22, the demo's idle drop-in at .08
   and .14 -- because there is no --accent-rgb and no color-mix in here. Same
   idiom, same hue, checked rather than assumed (tests/test_photo_tools_status).

   WHAT IT MEANS CHANGED on 2026-09-05, when the owner revised his own item
   the same day: "The the photos should always be available to the user to
   expand and edit but the photo tools will override it I guess." It briefly
   meant "you cannot touch this"; it now means "the tools are working on this
   one, and their version will land here". Nothing is disabled.

   Which is exactly why the editor's own filmstrip carries it too. The seller
   can rotate or crop a photo the tools hold, and their copy will replace what
   he does; the editor is where he does it, so a mark only on the page behind
   modal would warn him nowhere useful.

   BOTH SIGNALS, not one. The tint was removed on 2026-09-05 in favour of a
   #photo-review bar, on the argument that a wash sits on the thing the seller
   came to look at and that a sentence can say what a colour cannot. The
   sentence half of that is right and the bar stays. The tint comes back beside
   it because the two answer DIFFERENT questions: the bar says what is
   happening, and only the tint says WHICH photos it is happening to. A bar
   alone cannot point at three tiles out of five.

   The removal also left this comment unterminated -- a stray close marker here
   and five lines of prose outside any comment, 1,085 lines from the end of the
   file. That shipped. Repaired with the restore. */
/* WHITE, not blue. Owner 2026-09-06: "Replace the blue tint with a white one."
   It was blue by his own instruction the day before; a wash of the accent over
   a photograph carries the same hue as .sel, the selected border, and the two
   co-occur constantly -- the photo the tools are working on is very often the
   one the seller has open. White separates them completely: the edge is a
   choice about the picture, the wash is a state of it.

   It also stops the tint arguing with the photograph underneath. Blue at .24
   shifts every colour in the image toward it, so a seller judging a lens for
   yellowing was judging it through a filter. White lifts the whole tile
   evenly and leaves the hues where they are, which on a page about looking at
   parts is the difference that matters.

   Raised to .55 because white over a photograph reads as much weaker than the
   same alpha of a saturated colour -- at .24 it was invisible on a pale lens.
   The sheen is DARKENED now rather than lightened, for the same reason: a
   white sweep across a white wash is nothing at all. */
/* BLUE AGAIN (owner 2026-09-11: "Make all the loading tints BLUE", after the
   white of 2026-09-06). A light blue wash and a blue band sweeping it, the same
   tint as the title box, so one colour means "working" on the whole page. */
.thumb.working::after,.pm-tile.working::after{content:"";position:absolute;inset:0;
  border-radius:inherit;background:rgba(37,99,235,.08);pointer-events:none;z-index:1}
.thumb.working::before,.pm-tile.working::before{content:"";position:absolute;inset:0;
  border-radius:inherit;pointer-events:none;z-index:2;
  background:linear-gradient(100deg,transparent 30%,rgba(37,99,235,.16) 50%,transparent 70%);
  background-size:220% 100%;animation:thumb-work 1.5s linear infinite}
@keyframes thumb-work{from{background-position:120% 0}to{background-position:-120% 0}}
@media (prefers-reduced-motion:reduce){
  .thumb.working::before,.pm-tile.working::before{animation:none;background:none}}
.thumb.working .x{z-index:3}
/* The bar that replaced it. Same shell as the identify banner so the column
   does not change shape when both are up, and sitting above it with a margin
   rather than a border between them: two separate pieces of work, not one bar
   with a divider. Quieter than the identify banner on purpose -- that one is
   the answer the seller is waiting for, this one is a note about work he does
   not have to wait for at all. */
/* THE THREE BARS FORM ONE COLUMN. Owner 2026-09-06: "align the product
   thumbnail and what similar ones are listed for the top bars with the 'Need
   Review' blue tinted bar", after two earlier rounds of "the edges aren't
   aligning up".

   TWO MEASURED MISMATCHES, and they are different faults.

   1. THE TEXT INSET. This bar is .id-banner, whose padding is 16px 15px, so
      over its 1px border its text starts 16px in. Both headings it stacks
      above -- "Product Thumbnail" in .nl-photos (padding 25px) and "What
      similar ones are listed for" in .comps-head (padding 25px 25px 14px) --
      start 26px in over the card's own 1px border. Ten pixels, on three
      elements stacked directly on top of each other, which is exactly the
      offset the eye is best at catching and the hardest to name.

   2. THE WIDTH -- CONSIDERED AND DECLINED. The bar sits above .nl-grid, so it
      spans BOTH columns plus the 24px gap while the card below it spans one:
      left edges agree, right edges are a whole column apart. It was briefly
      narrowed to the photo column with calc(50% - 12px).

      OWNER CHOSE OTHERWISE, 2026-09-06, shown the variations side by side:
      "The text inset looks the best." So the bar keeps its full width and only
      the inset moves. Do not re-narrow it as a tidy-up -- the wide bar is the
      shipped answer, not an oversight, and it reads as a page-level notice
      spanning the work rather than a header belonging to the photo column.

      This is the whole change: one longhand overriding .id-banner's shorthand
      15px. .photo-review is declared after .id-banner and both are single
      classes, so source order carries it -- no specificity trick. */
.photo-review{margin-bottom:10px;background:var(--card);border:1px solid var(--line);
  align-items:flex-start;padding-left:25px}
.photo-review .idb-txt b{font-size:14px}
.photo-review .pr-sub{font-size:13px;color:var(--muted);line-height:1.45;margin-top:2px}

/* item specifics: label above field, two fields per row */
.spec-item{display:flex;flex-direction:column;gap:6px;min-width:0}
.spec-item .k{font-weight:600;font-size:14px;color:var(--ink)}
.spec-item .v{display:flex;align-items:center;gap:8px}
.spec-item .v input,.spec-item .v select{width:100%;border:1px solid var(--line);border-radius:6px;
  padding:10px 14px;font:inherit;font-size:14px;color:var(--ink-soft);background:#FBFBFE;outline:0;
  transition:border-color .12s,box-shadow .12s}
.spec-item .v input:focus,.spec-item .v select:focus{border-color:var(--accent);
  box-shadow:0 0 0 3px var(--accent-soft)}
.spec-item .v input.yr{width:auto;flex:1;min-width:0}
.yr-dash{color:var(--muted)}
.spec-grid:not(.show-adv) .spec-item.adv{display:none}
.spec-group-head{grid-column:1/-1;margin-top:4px}
.spec-group-head.more{margin-top:16px;padding-top:16px;border-top:1px solid var(--line)}
.spec-group-head h3{font-size:16px}
.spec-group-head h3 span{font-weight:400;color:var(--muted)}
.spec-group-head p{margin:3px 0 0;color:var(--muted);font-size:13px}

/* eBay-style multi-select (buildMultiSelect) */
.ms{position:relative;width:100%}
.ms-trigger{width:100%;display:flex;align-items:center;gap:8px;text-align:left;background:#FBFBFE;
  border:1px solid var(--line);border-radius:6px;padding:10px 14px;font:inherit;font-size:14px;
  color:var(--ink-soft)}
.ms-trigger:hover{border-color:var(--line-strong)}
.ms-trigger .ms-val{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ms-trigger .ms-ph{flex:1;color:var(--muted)}
.ms-trigger .ms-chev{width:17px;height:17px;flex:none;color:var(--muted)}
.ms-panel{position:absolute;top:calc(100% + 6px);left:0;right:0;z-index:40;background:#fff;
  border:1px solid var(--line);border-radius:6px;box-shadow:var(--shadow-lg);padding:12px;
  max-height:320px;overflow:auto}
.ms-search{width:100%;margin-bottom:8px;border:1px solid var(--line);border-radius:6px;
  padding:9px 12px;font:inherit;outline:0}
.ms-sel-head{display:flex;align-items:center;justify-content:space-between;margin:4px}
.ms-sel-head span{font-weight:700;font-size:14px}
.ms-clear{color:var(--accent);font-weight:600;font-size:14px}
.ms-selected:not(:empty){border-bottom:1px solid var(--line);margin-bottom:6px;padding-bottom:6px}
.ms-opt{width:100%;display:flex;align-items:center;gap:11px;padding:8px 6px;font:inherit;
  font-size:14px;text-align:left;border-radius:4px}
.ms-opt:hover{background:var(--bg)}
.ms-box{flex:none;width:19px;height:19px;border:2px solid var(--line-strong);border-radius:4px;
  display:grid;place-items:center;color:#fff}
.ms-opt.sel .ms-box{background:var(--accent);border-color:var(--accent)}
.ms-box svg{width:12px;height:12px}
.ms-add{width:100%;text-align:left;padding:8px 6px;font:inherit;font-size:14px;
  color:var(--accent);border-radius:4px}
.ms-add:hover{background:var(--tint)}

/* condition quick-picks */
.cq{display:flex;flex-direction:column;gap:5px}
.cq-lab{font-size:13px;font-weight:600;color:var(--ink-soft)}
.cq select{border:1px solid var(--line);border-radius:6px;padding:8px 11px;font:inherit;
  background:#fff;color:var(--ink);outline:0}

/* pending-choice markers under the title */
.title-choices{font-size:13px;color:var(--muted);align-items:center;margin-top:2px}
.title-choices .title-choice-seg{color:var(--ink-soft)}
.title-choice-marker{display:inline-flex;align-items:center;gap:3px;font-weight:600;font-size:12px;
  color:var(--ink-soft);background:var(--tint);border:1px solid var(--tint-line);border-radius:999px;
  padding:2px 9px}
.title-choice-marker:hover{filter:brightness(.97)}
/* Capped to the viewport, with the OPTIONS scrolling inside. Unit completion
   grew the offered surface ~15% on 2026-09-01 and this box had no max-height
   and no overflow, so a long list ran off the bottom of the screen carrying
   the Apply button with it -- options a seller could not reach and a button
   they could not press, with nothing saying why. */
.title-choice-popup{position:fixed;z-index:70;min-width:190px;max-width:270px;background:#fff;
  border:1px solid var(--line);border-radius:6px;box-shadow:var(--shadow-lg);padding:8px;
  display:flex;flex-direction:column;max-height:70vh}
/* min-height:0 is the load-bearing half: without it a flex child refuses to
   shrink below its content and overflows the parent instead of scrolling. */
.tcp-list{overflow-y:auto;min-height:0}
.title-choice-popup .tcp-title{font-size:11px;font-weight:700;letter-spacing:.04em;
  text-transform:uppercase;color:var(--muted);padding:4px 6px 6px}
.title-choice-popup .tcp-opt{width:100%;display:flex;align-items:center;gap:9px;padding:8px 6px;
  font:inherit;font-size:14px;text-align:left;border-radius:4px}
.title-choice-popup .tcp-opt:hover{background:var(--bg)}
.title-choice-popup .tcp-opt .tcp-box{flex:none;width:16px;height:16px;
  border:2px solid var(--line-strong);border-radius:4px;display:grid;place-items:center;color:#fff}
.title-choice-popup .tcp-opt.sel .tcp-box{background:var(--accent);border-color:var(--accent)}
.title-choice-popup .tcp-opt .tcp-box svg{width:11px;height:11px}
.title-choice-popup .tcp-apply{width:100%;margin-top:6px;flex:none;background:var(--btn);color:#fff;
  border-radius:6px;padding:8px;font-weight:700;font-size:13px}
.title-choice-popup .tcp-hint{padding:6px 6px 2px;font-size:11px;color:var(--muted);
  border-top:1px solid var(--line);margin-top:6px}

/* ---- generic buttons used by the modals/dialogs (carried from style.css) ---- */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;font-weight:600;
  font-size:14px;border:1px solid transparent;border-radius:4px;padding:10px 20px}
.btn-primary,.btn-accent{background:var(--btn);color:#fff}
.btn-primary:hover,.btn-accent:hover{filter:brightness(1.07)}
.btn:disabled{opacity:.45;cursor:not-allowed}
.btn svg{width:16px;height:16px}

/* ---- photo editor modal (pm-*, carried from style.css and re-tokenised) ---- */
.modal{position:fixed;inset:0;background:rgba(16,17,21,.5);display:grid;place-items:center;z-index:60}
.modal .panel{background:#fff;border-radius:6px;box-shadow:var(--shadow-lg);
  width:min(760px,93vw);max-height:88vh;overflow:auto;padding:24px}
/* Owner 2026-09-09: "Making the popout bigger can help by spreading the
   tools out." Wider AND taller: eight controls on one row need the width, and
   the stage should not shrink to pay for them. */
/* TWO CLASSES, DELIBERATELY. `.pm-large` alone (0-1-0) lost to `.modal .panel`
   (0-2-0) above, so the editor had been 760px wide the whole time the rule
   said 1280 -- and at 760 the tool row wraps and Apply drops below the fold
   (measured in headless Chrome 2026-09-10). */
.modal .pm-large{width:min(1280px,96vw)}
/* The in-app confirm that replaced window.confirm (owner 2026-08-31). Rides on
   .modal/.panel so it inherits the overlay, the width cap and the shadow; only
   the question's own layout is new. */
/* THE BAR, NOT A PANEL (owner 2026-09-11, board D: the bottom bar for every
   pop-up). No veil: the page stays readable behind the question. Focus trap,
   Escape and the backdrop click are unchanged; only the box moved. */
#confirm-modal.modal,.sd-modal.modal{background:transparent;align-items:end;padding-bottom:26px}
.modal .cf-panel,.modal .sd-panel{width:min(560px,92vw);max-height:none;padding:11px 14px;background:var(--accent);color:#fff;
  border-radius:0;box-shadow:none;display:flex;align-items:center;gap:14px;flex-wrap:wrap}
.cf-panel .cf-txt,.sd-panel .sd-txt{flex:1;min-width:200px}
.modal .cf-title,.modal .sd-panel h2{font:400 13px/1.5 var(--mono);color:#fff;margin:0;letter-spacing:0}
/* SOLID WHITE (2026-09-11 sweep): 85% white blended to #DEE8FC on the accent,
   4.19:1 at 12px, under AA's 4.5 -- axe found it with the dialogs open. The
   title still leads by size (13px against 12px). */
.modal .cf-body,.modal .sd-panel .sd-body{font:400 12px/1.5 var(--mono);color:#fff;margin:2px 0 0}
.modal .cf-row,.modal .sd-row{display:flex;gap:8px;margin:0;flex:none;flex-wrap:wrap;justify-content:flex-end}
.modal .cf-cancel,.modal .sd-panel .btn-soft{background:transparent;color:#fff;border:1px solid rgba(255,255,255,.45);padding:8px 12px;font-size:12px;font-weight:600;border-radius:0}
.modal .cf-ok,.modal .sd-panel .btn-blue{background:#fff;color:var(--accent);border:1px solid transparent;padding:8px 12px;font-size:12px;font-weight:600;border-radius:0}
.modal .cf-ok:hover,.modal .sd-panel .btn-blue:hover{background:#fff;color:var(--accent-hover)}
.modal .cf-ok.danger{background:#fff;color:var(--danger)}
.modal .cf-ok.danger:hover{color:var(--danger)}
@keyframes cf-up{from{opacity:0;transform:translateY(100%)}to{opacity:1;transform:none}}
.modal .cf-panel,.modal .sd-panel{animation:cf-up .28s cubic-bezier(.2,.8,.2,1);box-shadow:none;border-radius:0}
body:has(#view-listing:not([hidden])) #confirm-modal.modal,body:has(#view-listing:not([hidden])) .sd-modal.modal{padding-bottom:104px}
.cf-title{font-size:19px;font-weight:600;line-height:1.3;margin:0}
.cf-body{margin:10px 0 0;font-size:16px;line-height:1.5;color:var(--muted)}
.cf-row{display:flex;justify-content:flex-end;gap:10px;margin-top:22px}
.cf-row button{font:inherit;font-size:14px;font-weight:600;border-radius:6px;
  padding:9px 16px;cursor:pointer;border:1px solid var(--line)}
.cf-cancel{background:#fff;color:var(--ink)}
.cf-cancel:hover{background:var(--surface-2,#f4f5f7)}
.cf-ok{background:var(--btn,#2b6cf6);border-color:transparent;color:#fff}
.cf-ok:hover{background:var(--btn-hover,#1f5ae0)}
/* Destructive actions are red AND focus lands on Cancel, so the dangerous key
   is never the one already under Enter. */
.cf-ok.danger{background:var(--danger);border-color:transparent;color:#fff}
.cf-ok.danger:hover{filter:brightness(.92)}
.cf-panel{animation:cf-in .16s cubic-bezier(.2,.8,.2,1)}
@keyframes cf-in{from{opacity:0;transform:translateY(6px) scale(.98)}
  to{opacity:1;transform:none}}
@media (prefers-reduced-motion:reduce){.cf-panel{animation:none}}
.pm-head{display:flex;align-items:center;gap:12px;margin-bottom:14px}
.pm-head h2{font-size:19px}
.pm-main{display:flex;align-items:center;position:relative}
.pm-arrow{position:absolute;top:50%;transform:translateY(-50%);z-index:2;width:42px;height:42px;
  border-radius:50%;border:1px solid var(--line);background:#fff;font-size:22px;display:grid;
  place-items:center;color:var(--ink-soft);box-shadow:var(--shadow)}
.pm-arrow:first-child{left:14px}
.pm-arrow:last-child{right:14px}
.pm-arrow:hover{border-color:var(--accent);color:var(--accent)}
.pm-arrow:disabled{opacity:0;pointer-events:none}
.pm-stage{flex:1;background:var(--bg);border:1px solid var(--line);border-radius:6px;height:64vh;
  display:grid;place-items:center;overflow:hidden}
.pm-imgwrap{position:relative;display:inline-block;line-height:0}
#pm-preview{max-width:100%;max-height:64vh;display:block;user-select:none;-webkit-user-drag:none;
  border-radius:6px}
/* A CLASS, NOT AN ID. This was #pm-crop, and when the editor became one
   component mounted in two places (2026-09-09) the page's box was #pe-crop and
   matched nothing: not positioned, no border, never hidden. Dragging drew a
   rectangle that was invisible and in the wrong place, which from the seller's
   side is a crop tool that does nothing -- "The crop doesn't work".

   An id selector can only ever match one mount, so shared markup cannot be
   styled by one. */
/* THE BOX THE SELLER TIGHTENS. Owner 2026-09-09: "the crop should work where
   the user tightens the preset box around the picture. Not creating the
   boundaries themselves." It opens around the whole photo, so the surround is
   dark only once it has been pulled in.

   No border-radius: a crop is a rectangle, and rounded corners on the frame
   misdescribe what will be cut. */
.pe-cropbox{position:absolute;border:1px solid #fff;box-shadow:0 0 0 9999px rgba(16,17,21,.55);
  display:none;cursor:move}
/* Rule of thirds, the way every photo editor draws it. */
.pe-grid{position:absolute;inset:0;pointer-events:none;
  background-image:
    linear-gradient(to right,rgba(255,255,255,.35) 1px,transparent 1px),
    linear-gradient(to bottom,rgba(255,255,255,.35) 1px,transparent 1px);
  background-size:33.333% 33.333%;background-position:33.333% 33.333%;
  background-repeat:repeat}
/* The grips. Big enough to hit -- 14px of target for a 10px mark, because a
   handle you cannot grab is the whole tool failing. */
.pe-h{position:absolute;width:14px;height:14px;background:#fff;border-radius:4px;
  box-shadow:0 0 0 1px rgba(16,17,21,.35)}
.pe-h-nw{left:-7px;top:-7px;cursor:nwse-resize}
.pe-h-n{left:calc(50% - 7px);top:-7px;cursor:ns-resize}
.pe-h-ne{right:-7px;top:-7px;cursor:nesw-resize}
.pe-h-e{right:-7px;top:calc(50% - 7px);cursor:ew-resize}
.pe-h-se{right:-7px;bottom:-7px;cursor:nwse-resize}
.pe-h-s{left:calc(50% - 7px);bottom:-7px;cursor:ns-resize}
.pe-h-sw{left:-7px;bottom:-7px;cursor:nesw-resize}
.pe-h-w{left:-7px;top:calc(50% - 7px);cursor:ew-resize}
/* Crop mode is not a drawing mode any more, so the crosshair would lie. */
.pm-imgwrap.crop{cursor:default}
.pm-imgwrap.panning{cursor:grab}
.pm-imgwrap.panning:active{cursor:grabbing}
/* The transform is set inline by pePaint; the transition is only so a slider
   drag does not look like a series of jumps. */
#pm-preview{transform-origin:center center;transition:filter .08s linear}
@media (prefers-reduced-motion:reduce){
  #pm-preview{transition:none}
}
/* THE TOOL ROW. One height for everything on it (owner 2026-09-10: "its
   uneven and all over the place"): 34px controls on a 40px row, one radius,
   one accent, and the clusters sit on a shared tinted ground instead of
   being cut apart by hairlines. The sliders fix their label and readout
   widths so the two of them line up. */
.pm-tools{display:flex;align-items:center;flex-wrap:wrap;gap:10px;margin:14px 0;
  min-height:40px}
.pm-tools .sp{flex:1}
.pe-grp{display:inline-flex;align-items:center;gap:2px;padding:3px;
  border:1px solid var(--line);border-radius:var(--r);background:var(--bg)}
/* The commit pair stands on the page, not on the ground: Reset and Apply are
   the decisions, and a decision does not live in a cluster of adjustments. */
.pe-grp.pe-commit{padding:0;gap:8px;border-color:transparent;background:transparent;
  margin-left:auto}
/* Remove and Reset at the left edge (owner 2026-09-10); only Apply is pushed
   to the right. */
.pe-grp.pe-commit.pe-left{margin-left:0}
/* Icon-only, so a square: the name lives in the title and aria-label. */
.pm-tbtn.pm-ico{width:34px;padding:0;justify-content:center}
.pm-tbtn{display:inline-flex;align-items:center;gap:7px;height:34px;padding:0 12px;
  border:1px solid transparent;border-radius:6px;background:transparent;
  color:var(--ink-soft);font:inherit;font-size:13px;font-weight:600;
  white-space:nowrap;cursor:pointer;transition:background .12s,color .12s,border-color .12s}
.pm-tbtn:hover{background:#fff;border-color:var(--line);color:var(--ink)}
.pm-tbtn:active{transform:translateY(1px)}
.pm-tbtn:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
.pm-tbtn:disabled{opacity:.45;cursor:default;transform:none}
.pm-tbtn:disabled:hover{background:transparent;border-color:transparent;color:var(--ink-soft)}
.pm-tbtn svg{width:16px;height:16px;flex:none}
.pm-tbtn.danger{color:var(--danger)}
.pm-tbtn.on{background:var(--btn);color:#fff;border-color:var(--btn)}
.pm-tbtn.on:hover{background:var(--btn-hover,var(--btn));border-color:var(--btn-hover,var(--btn));color:#fff}
.pe-commit .pm-tbtn{height:36px;padding:0 16px;font-size:14px;background:#fff;
  border-color:var(--line)}
.pe-commit .pm-tbtn.on{background:var(--btn);border-color:var(--btn)}
.pe-commit .pm-tbtn:disabled:hover{background:#fff;border-color:var(--line)}
.pe-commit .pm-tbtn.on:disabled:hover{background:var(--btn);border-color:var(--btn);color:#fff}
.pe-sl{display:inline-flex;align-items:center;gap:8px;height:34px;padding:0 10px 0 8px;
  font-size:13px;font-weight:600;color:var(--ink-soft)}
.pe-sl>span{width:62px}
.pe-sl input[type=range]{width:120px;margin:0;accent-color:var(--btn)}
.pe-sl output{width:44px;text-align:right;font-variant-numeric:tabular-nums;color:var(--ink)}
/* Two deliberate rows rather than a ragged wrap: the spacer becomes a line
   break, so the adjustments sit on the first row and Remove / Reset / Apply
   on the second, with the commit pair still at the right. */
@media (max-width:900px){
  .pm-tools .sp{flex:1 1 100%;height:0}
}
@media (prefers-reduced-motion:reduce){
  .pm-tbtn{transition:none}
  .pm-tbtn:active{transform:none}
}
.pm-film{display:flex;gap:9px;overflow-x:auto;padding:12px 2px 4px;border-top:1px solid var(--line)}
.pm-tile{flex:none;width:90px;height:66px;border-radius:6px;background:var(--bg) center/cover no-repeat;
  border:2px solid transparent;cursor:pointer;position:relative}
.pm-tile:hover{border-color:var(--line-strong)}
.pm-tile.sel{border-color:var(--accent)}
.pm-tile .n{position:absolute;left:4px;top:3px;font-size:11px;font-weight:700;color:#fff;
  background:rgba(16,17,21,.55);border-radius:4px;padding:0 6px}
.pm-tile.dragging{opacity:.4;transform:scale(.92)}

/* ============ drafts ============ */
.drafts-tools{margin-left:auto;align-self:center;display:flex;align-items:center;gap:14px;flex-wrap:wrap}
/* An explicit display beats the UA stylesheet's [hidden]{display:none}, so
   without this the element stays on screen with .hidden reading true — which
   is exactly what a scripted check would call a pass. */
.drafts-tools[hidden]{display:none}
/* ONE HEIGHT ACROSS THE ROW. Owner 2026-09-09, with a screenshot: "The
   'select all' button within drafts should be the same size as the other boxes
   next to it".

   The three sat at three heights because each took its own class's padding --
   .btn-soft 8px, .btn-blue 7px, and .btn-pill re-padded to 13px further down
   this file. Matching them by editing three paddings would break again the
   next time one of those classes moved, so the ROW sets the height and each
   button keeps its own horizontal padding and colour. */
.drafts-tools > button{min-height:42px;padding-top:0;padding-bottom:0;
  font-size:14px;line-height:1}
.sel-all{padding-left:16px;padding-right:16px}
.draft-search{max-width:420px;margin-bottom:18px}
#drafts-ready-chip{font-size:12px;padding:3px 10px}
.pcard .prow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;row-gap:6px}
.pcard .psel{width:16px;height:16px;accent-color:var(--accent);cursor:pointer}
.pcard .pmeta{margin-left:auto;font-size:12px}
/* WHOLE CHIPS (2026-09-11 sweep): with a checkbox and two chips in the row,
   "Missing info", "Needs review" and "0 photos" each broke onto two lines
   inside themselves. They stay whole now and the row wraps between them. */
.pcard .prow .chip,.pcard .prow .pmeta{white-space:nowrap}
.pcard.pinned{border-color:var(--accent)}
/* A ready draft is selectable anywhere on the card, so the card has to say so:
   the cursor and a lift on hover, a full accent border once it is picked. Listed
   after .pinned deliberately — a pinned card that is also selected should read
   as selected. */
.pcard.selectable{cursor:pointer}
.pcard.selectable:hover{border-color:var(--accent)}
.pcard.is-sel,.pcard.is-sel:hover{border-color:var(--accent);
  box-shadow:0 0 0 1px var(--accent) inset}
/* ============ preferences ============ */
#prefs-body .card,#prefs-body section{background:#fff;border:1px solid var(--line);
  border-radius:6px;box-shadow:none;padding:25px;margin-bottom:16px}
#prefs-body h2,#prefs-body h3{font-size:19px;margin-bottom:16px}
#prefs-body input[type=text],#prefs-body textarea,#prefs-body select{border:1px solid var(--line);
  border-radius:6px;padding:10px 14px;font:inherit;font-size:14px;color:var(--ink-soft);
  background:#FBFBFE;width:100%}
#prefs-body button{border-radius:999px}
/* prefs field labels use plain .spec-grid rows (not .spec-item), so the shared
   .spec-item .k rule never applied to them — style them directly here */
#prefs-body .spec-grid .k{font-weight:600;font-size:14px;color:var(--ink);align-self:center}
.dash-grid > .muted{grid-column:1/-1}
/* The dashboard's empty slot is a section, not a whole screen, so it stays a
   line of prose with the way in inside it — the full .empty-state treatment
   belongs to Drafts, where empty IS the screen. */
.dash-empty{grid-column:1/-1;color:var(--muted);font-size:14px}
.dash-empty a{color:var(--accent);font-weight:600;cursor:pointer}
.dash-empty a:hover{text-decoration:underline}

/* ============ white app sidebar (2026-08 designer mocks) ============ */
.conn-card{background:var(--bg)}
.conn-card:hover{border-color:var(--line-strong)}

/* ============ next-first listing bar + dashboard polish (2026-08) ============ */
.nl-bar .nl-list-sm{flex:none;padding:8px 16px;font-size:14px}
/* Two lines of explanation became a mark you can ask (owner 2026-09-03:
   "Compact the informational on the next button to a question hover"). Same
   words, in the title, so it is one hover on a desktop and one long-press on a
   touchscreen -- and it no longer competes with the button it describes. */
/* The question mark is gone (owner 2026-09-03). Three attempts at placing a
   mark that explains Next -- two lines beside it, a 24px circle before it, a
   17px one after it -- and each read as one more thing to deal with on the way
   to the button. The words live in the buttons' own title now, which is where
   a hover explanation belongs when the control is already labelled. */
/* One shape for both (owner 2026-09-03: "make the 'new' and next button the
   same"). New was a btn-soft at 62x39 beside a btn-pill at 83x34: different
   heights, different corners, in a bar that now holds nothing else. */
.nl-bar #btn-new,.nl-bar .btn-pill{min-height:38px;font-size:16px;
  border-radius:4px;display:inline-flex;align-items:center;justify-content:center}
.nl-bar #btn-new{padding:5px 20px}
.nl-bar .btn-pill{padding:5px 5px 5px 20px;font-size:16px}
.pcard .pdel{position:absolute;top:14px;right:14px;width:30px;height:30px;border-radius:50%;
  background:#fff;border:1px solid var(--line);display:flex;align-items:center;justify-content:center;
  opacity:0;transition:opacity .15s;z-index:2;box-shadow:var(--shadow)}
.pcard:hover .pdel{opacity:1}
.pcard .pdel svg{width:14px;height:14px;stroke:var(--danger)}

/* ============ settings view (2026-08 designer mocks) ============ */
#settings-body .card{padding:25px;margin-bottom:16px}
/* A trailing margin on the last child lands on top of the card's own bottom
   padding, which is what made these boxes sit 6-10px lower than their equal
   top. Nothing follows it inside the card, so the margin has nothing to
   separate it from. */
#prefs-body .card>*:last-child,#prefs-body section>*:last-child,
#settings-body .card>*:last-child{margin-bottom:0}
/* Kept as its own rule: these cards keep [hidden] rows parked at the end, so
   :last-child above lands on something nobody can see and the visible last row
   keeps its margin. A browser without `of S` support drops only this block. */
#prefs-body .card>*:nth-last-child(1 of :not([hidden])),
#prefs-body section>*:nth-last-child(1 of :not([hidden])),
#settings-body .card>*:nth-last-child(1 of :not([hidden])){margin-bottom:0}
/* An inline-block control sits on the text baseline and reserves room for the
   descender under it -- 7px of gap below a textarea that nothing draws in. */
textarea.input{vertical-align:top}
#settings-body h2{font-size:19px;margin-bottom:16px}
.acct-row{display:flex;align-items:center;gap:14px;border:0;
  border-radius:6px;padding:15px;background:var(--bg);flex-wrap:wrap}
.acct-avatar{width:46px;height:46px;border-radius:6px;background:var(--btn);color:#fff;
  display:grid;place-items:center;font-weight:700;font-size:16px;flex:none}
.acct-row .acct-txt{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1}
.acct-row .acct-name{display:flex;align-items:center;gap:9px;font-weight:700;font-size:16px}
.acct-row .acct-name .chip{text-transform:uppercase;letter-spacing:.03em}
.acct-row .acct-sub{color:var(--muted);font-size:14px}
.acct-row .acct-btns{display:flex;gap:9px;flex-wrap:wrap}
.acct-row .btn-soft{border:0}
#settings-body .set-row .btn-soft{background:#F2F3FC;border:0;color:var(--ink-soft)}
#settings-body .set-row .btn-soft:hover{background:#E9EBF9}
.btn-danger-soft{display:inline-flex;align-items:center;justify-content:center;gap:8px;
  background:#FCE8E9;color:var(--danger);border-radius:999px;padding:8px 16px;
  font-weight:600;font-size:14px}
.btn-danger-soft:hover{filter:brightness(.97)}
/* ---- delete account + its confirmation dialog ----
   A solid red button, not the soft one: this is the only action in the app that
   cannot be undone, and it should not look like Disconnect. It carries the
   warning on its own -- no fenced-off section (owner 2026-08-27). */
.btn-danger{display:inline-flex;align-items:center;justify-content:center;gap:8px;
  background:var(--danger);color:#fff;border:0;border-radius:999px;padding:9px 18px;
  font-weight:600;font-size:14px;cursor:pointer}
.btn-danger:hover{filter:brightness(.94)}
.btn-danger:disabled{background:#EDEFF8;color:var(--muted);cursor:default;filter:none}
.dz-panel{width:min(480px,93vw)}
.dz-panel h2{font-size:22px;margin-bottom:6px}
.dz-warn{color:var(--danger);font-weight:600;font-size:14px;margin-bottom:12px}
.dz-body{color:var(--ink-soft);font-size:14px;line-height:1.5;margin-bottom:18px}
.dz-l{display:block;font-size:14px;font-weight:600;margin:0 0 6px}
.dz-panel .input{width:100%;margin-bottom:14px}
.dz-hint{color:var(--muted);font-size:13px;line-height:1.45;margin:-8px 0 16px}
.dz-err{color:var(--danger);font-size:14px;margin:0 0 14px}
.dz-btns{display:flex;gap:10px;justify-content:flex-end;flex-wrap:wrap}

/* ---- the deletion is running ----
   Replaces the form in the same panel. Nothing to press, so nothing that looks
   pressable; the ring in the corner is the only moving part and the sentence
   above it carries the instruction. tabindex="-1" makes it focusable without
   putting it in the tab order -- focus is moved here because the button that
   had it is hidden by then. */
.dz-prog{outline:none}
/* A step down from the 22px the question above it used: this is the answer to
   a decision already taken, not another thing to weigh. */
.dz-prog h2{font-size:19px;margin-bottom:8px}
/* The one line a seller must not miss, so it is the one line that is not grey.
   Weight rather than colour: red would read as an error, and nothing is wrong. */
.dz-prog-stay{color:var(--ink);font-size:14px;font-weight:600;line-height:1.5;
  margin:0 0 4px}
.dz-prog-foot{display:flex;justify-content:flex-end;margin-top:14px}
/* The ring itself is styled beside .load-mark, not here -- an override that
   sits before the rule it overrides is a puzzle for whoever reads it next. */

/* ---- developer mode panel (admin account only) ----
   Deliberately unlike the rest of the app: this is a tool, not a surface a
   seller ever sees, and it should be obvious at a glance that it is not part
   of the product. It does not auto-dismiss. */
#dev-detail{position:fixed;left:16px;bottom:16px;z-index:80;max-width:min(620px,calc(100vw - 32px));
  background:#14151A;color:#E8E9F0;border-radius:6px;box-shadow:0 18px 44px rgba(0,0,0,.42);
  font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;overflow:hidden}
#dev-detail .dd-head{display:flex;align-items:center;gap:9px;padding:8px 10px;
  background:#1D1F26;font-size:12px}
#dev-detail .dd-tag{background:var(--danger);color:#fff;border-radius:6px;
  padding:1px 6px;font-weight:700;letter-spacing:.04em;text-transform:uppercase}
#dev-detail .dd-path{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;
  white-space:nowrap;color:#A6A9B8}
#dev-detail .dd-copy,#dev-detail .dd-x{background:transparent;border:0;cursor:pointer;
  color:#A6A9B8;font-family:inherit;font-size:12px;padding:2px 5px;border-radius:6px}
#dev-detail .dd-x{font-size:16px;line-height:1}
#dev-detail .dd-copy:hover,#dev-detail .dd-x:hover{background:#2A2D36;color:#fff}
#dev-detail .dd-body{margin:0;padding:10px;font-size:12px;line-height:1.5;
  white-space:pre-wrap;word-break:break-word;max-height:38vh;overflow:auto}
@media (max-width:640px){#dev-detail{left:8px;right:8px;bottom:8px;max-width:none}}

.set-row{display:flex;align-items:center;gap:16px;padding:8px 0}
.set-row .set-txt{flex:1;min-width:0}
.set-row .set-t{font-weight:600;font-size:14px}
.set-row .set-d{color:var(--muted);font-size:14px;margin-top:0}
/* Three support actions sized to their own labels left a ragged right edge,
   because .set-row lets the trailing element be as wide as its words. They
   share a column instead. */
.card.support .set-row > a{min-width:156px}

/* ============ preferences (2026-08 designer mocks) ============ */
/* The condition block, secondary inside the Description card (owner
   2026-09-09). A rule above it rather than a heading weight, because it is the
   same card continuing and not a new subject: the seller reads one thing that
   has a second part, which is what collapsing the two cards was for. */
.tmpl-sub{margin-top:18px;padding-top:16px;border-top:1px solid var(--line)}
.tmpl-sub-h{display:block;font-weight:600;font-size:14px;margin-bottom:4px}
.tmpl-sub .cond-tmpl{min-height:150px}
/* The Save button follows the seller down the page, floating over it. Owner
   2026-09-09: "make the save changes bar an overlay instead of integrated
   within the page", after 2026-09-09: "Move 'apply' changes to stay stuck to
   the bottom of the screen".

   FIXED AND RIGHT-ANCHORED, matching .nl-bar, which is the same idea on the
   listing page. Right-anchored rather than centred because the content column
   sits beside a nav that collapses: a bar centred on the VIEWPORT is off-centre
   against the content, and by a different amount in each nav state. Anchoring
   to the right edge is true in both.

   IT WAS STICKY UNTIL NOW, and the one thing sticky gave for free has to be
   paid for here: an in-flow element cannot cover the last control, a fixed one
   can. The two views that carry a bar get bottom room to clear it, which is the
   cost of the overlay and is stated rather than discovered. */
.save-bar{position:fixed;right:24px;bottom:18px;z-index:30;
  display:flex;align-items:center;gap:14px;
  padding:14px;border-radius:6px;
  background:var(--card);box-shadow:var(--shadow-lg)}
/* Room for the overlay to sit over nothing that matters. */
#view-prefs,#view-settings{padding-bottom:96px}
/* An empty status span still takes its share of the gap and pushes the button
   off the padding -- the same trap .nl-bar .status:empty records. */
/* The message is a sibling in a small bar now, not a line pushed to the far
   side of a page-wide one. */
#view-prefs .page-head .btn-blue.round{align-self:center}
.fld-label{font-weight:600;font-size:14px;color:var(--ink)}
/* iOS-style switch (mock toggles) — input stays real for the JS, track is drawn */
.switch{position:relative;display:inline-flex;flex:none;cursor:pointer}
.switch input{position:absolute;inset:0;opacity:0;margin:0;cursor:pointer}
.switch .track{width:42px;height:24px;border-radius:999px;background:#E1E3EF;
  transition:background .15s;display:inline-block;pointer-events:none}
.switch .track::after{content:"";position:absolute;top:3px;left:3px;width:18px;height:18px;
  border-radius:50%;background:#fff;box-shadow:0 1px 3px rgba(16,17,21,.25);transition:transform .15s}
.switch input:checked+.track{background:var(--accent)}
.switch input:checked+.track::after{transform:translateX(18px)}
.switch input:focus-visible+.track{box-shadow:0 0 0 3px var(--tint)}
/* "round" is now a SIZE, not a shape -- the app is on the 4px scale with
   everything else (owner 2026-09-03, wanting the site uniform). */
.btn-blue.round{border-radius:4px;padding:10px 22px;font-size:16px}
.pol-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}
.pol-grid .field{margin:0}
/* Item location: the cities a ZIP straddles, offered rather than picked for
   the seller, and the one line under the card that says what the lookup could
   and could not tell. Buttons and not a menu -- there are two or three of
   them, and a menu would hide the answer behind a click for nothing. [hidden]
   has to be said again because display:flex would otherwise win it. */
.loc-cities{display:flex;flex-wrap:wrap;gap:8px;margin:14px 0 10px}
.loc-cities[hidden]{display:none}
.loc-cities .btn-soft{padding:6px 13px;font-size:13px}
.loc-msg{margin:0;font-size:13px;line-height:1.5;color:var(--muted);
  min-height:19px}
.loc-msg.err{color:var(--danger-ink)}
/* The condition template is written line by line, so it reads as a block: a
   monospaced measure keeps the categories aligned, and the placeholder example
   is set solid enough to be read rather than guessed at (owner). */
.cond-tmpl{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  font-size:13px;line-height:1.75}
/* Solid, not a hint: these placeholders are worked examples the seller is meant
   to read and copy the shape of, so they carry body weight rather than the
   global muted grey — that applies to the description template as much as the
   condition one, and both are now fully opaque at the same colour. */
.cond-tmpl::placeholder,#tmpl::placeholder{color:var(--ink);opacity:1}
.cond-tmpl,#tmpl{color:var(--ink);opacity:1}

/* ---- template editing ----
   Both boxes grow to their content in JS (autoGrow), so the height set here is
   only the floor before the first measure — never a cap, or a long template
   would be read six lines at a time through an inner scrollbar again. */
.tmpl-box{font-size:14px;line-height:1.6;min-height:190px;resize:vertical}
.cond-tmpl{min-height:190px;resize:vertical}
.tmpl-expand{flex:none;padding:6px 12px;font-size:13px;gap:6px}
.tmpl-expand svg{width:14px;height:14px}

/* The pop-out. Near-full-viewport on purpose: the point is to stop metering the
   template through a letterbox. Selectors are two-deep to match `.modal .panel`
   and `.modal .panel textarea` -- at one class they lose to the base modal and
   the box silently collapses to a few lines, which is the bug this replaces. */
.modal .tmpl-panel{width:min(1040px,94vw);height:min(92vh,900px);max-height:92vh;
  display:flex;flex-direction:column;gap:12px;overflow:hidden}
.tmpl-panel .tmpl-head{display:flex;align-items:center;gap:12px;flex:none}
.tmpl-panel .tmpl-head h2{flex:1;font-size:19px;margin:0}
/* The page must not scroll behind an open dialog; lockScroll() adds this and
   compensates for the scrollbar width so the layout does not jump sideways. */
body.no-scroll{overflow:hidden}

/* ---- item specifics card ----
   A list of what gets filled in, not a form to fill in: name and how it is
   sourced on the left, the one editable thing in the middle, the include
   switch on the right. Three columns so the switches line up down the page. */
.is-lede{margin-bottom:14px}
/* Collapsed by default (owner 2026-08-27): the list is reference, opened only
   to add one or turn one off. <details> so it works without JS and answers to
   the keyboard on its own. */
.is-own-list:empty{display:none}
.is-empty{color:var(--muted);font-size:14px;padding:2px 0 2px}
.is-details{border-top:1px solid var(--line);margin-top:16px;padding-top:4px}
.is-summary{cursor:pointer;padding:9px 0;font-size:14px;font-weight:600;
  color:var(--accent);list-style:none;display:flex;align-items:center;gap:7px}
.is-summary::-webkit-details-marker{display:none}
.is-summary::before{content:"";width:0;height:0;border-left:5px solid currentColor;
  border-top:4px solid transparent;border-bottom:4px solid transparent;
  transition:transform .15s ease}
.is-details[open] .is-summary::before{transform:rotate(90deg)}
.is-summary:focus-visible{outline:2px solid var(--accent);outline-offset:3px;border-radius:6px}
@media (prefers-reduced-motion:reduce){.is-summary::before{transition:none}}
.is-group{font-size:11px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;
  color:var(--muted);margin:14px 0 2px}
.is-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column}
.is-row{display:grid;grid-template-columns:1fr 190px 46px;align-items:center;gap:16px;
  padding:11px 0;border-top:1px solid var(--line)}
.is-row:first-child{border-top:0}
.is-txt{min-width:0}
/* WRAPS ANYWHERE (2026-09-11 sweep): a 65-character name with no spaces -- eBay's
   limit, core/models.MAX_SPECIFIC_NAME -- ran 20px under the value box at 1024px.
   `anywhere`, not `break-word`: only `anywhere` lowers the min-content width of
   the anonymous flex item the text becomes, which is what lets it shrink. */
.is-name{font-weight:600;font-size:14px;display:flex;align-items:center;gap:7px;overflow-wrap:anywhere;
  flex-wrap:wrap}
.is-src{color:var(--muted);font-size:13px;margin-top:1px}
.is-default{font-size:14px;padding:7px 10px}
.is-remove{border:0;background:transparent;color:var(--muted);font-size:19px;line-height:1;
  cursor:pointer;border-radius:4px;width:30px;height:30px;justify-self:center}
.is-remove:hover{background:#FCE8E9;color:var(--danger)}
.is-add{display:grid;grid-template-columns:1fr 1fr auto;gap:12px;align-items:end;
  margin-top:12px}
/* .field carries margin-bottom:16px, and align-items:end aligns the MARGIN box
   -- which is why the inputs sat 16px above the button. Zero it here, and give
   the button the input's own padding, font-size and border so the two boxes
   compute to the same height instead of matching a hardcoded one. */
.is-add .field{min-width:0;margin-bottom:0}
.is-add .btn-soft{padding:10px 18px;font-size:14px}
.is-add-err{margin-top:8px;font-size:13px}
.is-add-err:empty{display:none}
.is-note{margin-top:14px}
@media (max-width:640px){
  .is-row{grid-template-columns:1fr 46px;row-gap:8px}
  .is-default{grid-column:1/-1;order:3}
  .is-add{grid-template-columns:1fr}
}

/* SMS consent disclosures. Small and quiet -- it is a legal requirement at the
   point of collection, not something to sell -- but never below the contrast
   floor, because a disclosure nobody can read is not a disclosure. */
.sec-sms-terms{margin:10px 0 0;max-width:46ch;font-size:12px;line-height:1.5;
  color:var(--ink-soft)}
.sec-sms-terms a{color:var(--ink-soft);text-decoration:underline;
  text-underline-offset:2px}
.sec-sms-terms a:hover{color:var(--accent)}

/* ---- title style card ----
   The chips are the title, read left to right, so they lay out as a line that
   wraps rather than a list. Each is focusable: drag alone is unreachable by
   keyboard, and this repo targets WCAG 2.1 AA. */
.ts-lede{margin-bottom:12px}
.ts-chips{display:flex;flex-wrap:wrap;gap:8px;list-style:none;margin:0 0 16px;padding:0}
.ts-chip{display:inline-flex;align-items:center;gap:7px;background:#F2F3FC;border:1px solid var(--line);
  border-radius:6px;padding:8px 12px;font-size:14px;font-weight:600;color:var(--ink);
  cursor:grab;user-select:none;min-height:36px}
.ts-chip:hover{border-color:var(--line-strong)}
.ts-chip:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.ts-chip.dragging{opacity:.4;cursor:grabbing}
/* Where the chip will land. Same idiom as the photo strip (.thumb.drop-*) so
   dragging feels identical in both places: a box-shadow bar on one side, which
   follows the border radius and so wraps slightly onto the corners. */
.ts-chip.drop-before{box-shadow:-4px 0 0 0 var(--accent)}
.ts-chip.drop-after{box-shadow:4px 0 0 0 var(--accent)}
.ts-chip .ts-grip{color:var(--muted);font-size:12px;letter-spacing:-1px}
.ts-chip .ts-edit{color:var(--accent);font-size:11px}
.ts-chip[data-token="side"]{cursor:pointer}
/* A part that cannot render on its own: still in the order, still draggable,
   but visibly not spending characters today. */
.ts-chip.is-off{background:var(--bg);color:var(--muted)}
.ts-chip.is-off .ts-edit{color:var(--muted)}
/* The "?" on a chip whose part does not show up in the worked preview. It sits
   inside a draggable chip, so it is a real button with its own focus ring. */
.ts-why{width:16px;height:16px;flex:0 0 16px;padding:0;border:1px solid var(--line-strong);
  border-radius:999px;background:var(--card);color:var(--muted);font:inherit;font-size:11px;
  font-weight:700;line-height:1;cursor:help;display:inline-flex;align-items:center;
  justify-content:center}
.ts-why:hover{border-color:var(--accent);color:var(--accent)}
.ts-why:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.ts-chip,.hint{position:relative}
.hint{display:inline-flex;align-items:center}
.ts-tip{position:absolute;left:0;bottom:calc(100% + 8px);z-index:5;width:270px;
  padding:10px 12px;border:1px solid var(--line);border-radius:6px;background:var(--card);
  box-shadow:0 6px 20px rgba(16,17,21,.14);font-size:13px;font-weight:400;line-height:18px;
  color:var(--ink);white-space:normal;text-align:left;cursor:default;
  opacity:0;visibility:hidden;transition:opacity .12s ease}
.ts-why:hover + .ts-tip,.ts-why:focus-visible + .ts-tip,
.ts-chip.tip-open .ts-tip,.hint.tip-open .ts-tip{opacity:1;visibility:visible}
/* Kept inside the card by tsPlaceTip(), which measures: the chips wrap, so a
   chip's position in the list says nothing about where it sits on screen. */
@media (prefers-reduced-motion:reduce){.ts-tip{transition:none}}
@media (max-width:640px){.ts-tip{width:min(270px,74vw)}}

.ts-side{border:1px solid var(--line);border-radius:6px;padding:14px 16px;margin-bottom:16px;
  background:#FBFBFF}
.ts-side-head{display:flex;align-items:center;margin-bottom:10px;font-size:14px}
.ts-side-head .ts-side-close{margin-left:auto}

.ts-preview{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;margin-top:14px;
  padding:12px 14px;border:1px solid var(--line);border-radius:6px;background:#fff}
.ts-preview-l{font-size:11px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;
  color:var(--muted);flex:none}
.ts-preview-t{flex:1;min-width:0;font-size:14px;font-weight:600;line-height:1.4;
  word-break:break-word}
.ts-preview-n{font-size:12px;color:var(--muted);font-variant-numeric:tabular-nums;flex:none}
.ts-note{margin-top:10px}
@media (max-width:640px){.ts-chip{font-size:13px;padding:8px 10px}}

/* The campaign select is alone on its row now that the ad rate is inherited
   rather than typed, so it should not sit in a two-column grid half-empty. */
.pl-row-solo .field{max-width:640px}
.pl-rate-note{margin-top:7px;font-size:13px;color:var(--muted)}
.pl-rate-note.err{color:var(--danger)}
/* Expanding must not change how the template looks (owner 2026-08-27): the
   pop-out was monospace while the in-page box uses the app's own face, so the
   same text read differently in the two places. Same family, size and leading
   as .tmpl-box now -- only the amount of room changes. */
/* The pop-out is the listing page's read-only frame (expandDescription) for
   the template as well since 2026-09-10; the editor stays in its card. The
   frame's sizing is .tmpl-panel .desc-big-frame further down. */
@media (max-width:640px){
  .modal .tmpl-panel{width:100%;height:100vh;max-height:100vh;border-radius:0}
}

/* Campaign + ad rate. The rate was pinned to 90px beside a full-width select,
   so a two-character field hung off the end of the row (owner). Give it a
   readable share and let the pair sit on one line down to tablet width. */
.pol-grid.pl-row{grid-template-columns:minmax(0,1fr) 160px;margin:6px 0 12px;align-items:end}
@media (max-width:760px){.pol-grid.pl-row{grid-template-columns:minmax(0,1fr)}}
.card-head-row{display:flex;align-items:center;gap:12px;margin-bottom:4px}
.card-head-row h2,.card-head-row .label{flex:1;margin-bottom:0!important}
.card-hint{color:var(--muted);font-size:14px}
/* .err and .ok existed only under .corr-panel, so every other status line that
   set className="err" -- the Promoted Listings campaign fetch among them --
   rendered a real eBay failure in the same grey as an explanatory hint. */
.err{color:var(--danger)}
/* the price's own line, under the box (2026-09-11 sweep) */
.nl-price-err{margin:6px 0 0;font-size:13px;font-weight:600}
.ok{color:var(--success)}
.cond-msg-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-top:16px}
.cond-msg-grid .field{margin:0}
.cond-msg-grid textarea.input{min-height:67px}
.cp-group{display:grid;grid-template-columns:237px 1fr 44px;gap:16px;align-items:center;
  margin-bottom:16px}
.cp-group .cp-hint{grid-column:2}
.cp-del{width:44px;height:44px;border-radius:50%;background:#FCE8E9;color:var(--danger);
  display:inline-flex;align-items:center;justify-content:center;flex:none}
.cp-del svg{width:15px;height:15px;stroke:var(--danger)}
.cp-add{display:inline-flex;align-items:center;gap:8px;background:#F2F3FC;color:var(--ink);
  border-radius:999px;padding:12px 24px;font-weight:600;font-size:14px}
.cp-add:hover{background:#E9EBF9}
@media (max-width:900px){.pol-grid,.cond-msg-grid{grid-template-columns:1fr}
  .cp-group{grid-template-columns:1fr 1fr 44px}}

/* ============ listing bar summary + price icon (2026-08 mocks) ============ */
.nl-bar-sum{display:flex;align-items:center;gap:8px}
.nl-bar-sum .bs-thumb{width:42px;height:42px;border-radius:6px;flex:none;
  background:var(--bg) center/contain no-repeat}
.nl-bar-sum .bs-txt{display:flex;flex-direction:column;gap:2px;min-width:0}
.nl-bar-sum .bs-t{font-weight:700;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.nl-bar-sum .bs-d{color:var(--muted);font-weight:400;font-size:13px;white-space:nowrap;
  overflow:hidden;text-overflow:ellipsis}
.nl-price img{width:15px;height:15px}

/* ============ marketing footer legal row ============ */
.mk-footer .mk-legal{display:flex;gap:18px}
/* White, on the blue plate this used to be. The plate is white now, so these
   take the page's own ink -- and keep the 24px target height they were given
   when they measured 20px, under the minimum. */
.mk-footer .mk-legal a{color:#101115;font-size:14px;font-weight:600;
  min-height:24px;display:inline-flex;align-items:center;opacity:.82}
.mk-footer .mk-legal a:hover{opacity:1}


/* ======== shared editorial pass 2026-08-17 ========
   One radius scale across the marketing pages — 10px surfaces, 8px nested,
   7px controls — hairline edges rather than soft coloured shadows, and the
   nav pill monochrome so it stops being the loudest thing above the fold.
   The app overrides these with a sharper scale in index.html. */
.btn-pill{border-radius:4px;padding:13px 22px;gap:9px}
.btn-pill:hover{transform:none;box-shadow:none;background:#26272B}
.btn-pill .ic{width:auto;height:auto;background:none;border-radius:0}
.btn-pill .ic svg{width:15px;height:15px;stroke:#fff}
.btn-pill.light{background:#fff;color:var(--ink)}
.btn-pill.light:hover{background:#E9E9EC}
.btn-pill.light .ic svg{stroke:var(--ink)}
.card{border-radius:6px;box-shadow:none}
.mk-hero h1{font-weight:700;letter-spacing:-.022em}
/* This block used to repaint the footer for the dark plate. The plate is white
   now (2026-09-04), so what is left is the square nav chip the editorial pass
   wanted; the colours come from the rule above rather than being restated
   here, which is what let the two disagree the last two times. */
.mk-footer nav{border-radius:6px;padding:6px}
.mk-footer nav a{border-radius:4px}

/* ===== site consistency pass 2026-08-20 =====
   Measured drift, fixed at the shared layer rather than per page so it cannot
   come apart again the next time a page gets its own <style> block. */

/* h1 was 800 on home, 700 on early-access/privacy/terms, and 600 on contact and
   the 404 — two pages with visibly lighter headlines than their siblings for no
   reason anyone chose. The hero keeps 800; every other page headline is 700. */
body.mk h1,body.auth h1{font-weight:700}
/* body.mk h1 is (0,1,2); a bare .hh-copy h1 is (0,1,1) and LOSES to it. Matched
   depth. 800 became 700 on 2026-09-10 with Red Hat Display, which is loaded at
   700 only. */
body.mk .hh-copy h1{font-weight:700}

/* ===== public restyle 2026-09-10 =====
   Owner, after the demo-frame drafts and the coming-soon studies: Draft A
   (claim centred, the product in a browser frame at full width), the
   skeleton in the frame until the walkthrough exists, Red Hat Display 700
   for display type, the two-tone wordmark, square corners on every box --
   "apply the change throughout the whole website besides the in app".

   EVERYTHING HERE IS SCOPED TO body.mk / body.auth. The app's <body> carries
   no class, so it keeps Archivo, the tile lockup and the 6px/4px scale; the
   --display token is overridden rather than changed for the same reason. */
/* The tokens above carry the face and the zero radius for every page since the
   app joined (2026-09-10); nothing is scoped any more. */
/* NO ROUNDEDNESS ANYWHERE (owner 2026-09-10: "All boxes have to be sharp, no
   roundedness anywhere"). Not a list of selectors, which is how the window
   dots and a page's own rule kept slipping through: every element and every
   pseudo-element on a public page is a rectangle. BOXES ONLY: the icons and
   glyphs are drawn as the designer drew them (owner, the same evening, on a
   pass that redrew them: "You change all the symbols etc. Revert please").
   The app joined on 2026-09-10 ("i mean the in-app side"), so
   this is every page. */
*,*::before,*::after{border-radius:0!important}
/* ICONS STAY CIRCULAR (owner 2026-09-10: "make sure all icons are circular.
   the login page has a square loading icon"). The blanket above is for boxes;
   a spinner, a round icon button, a toggle's knob, the window dots and a list
   bullet are drawings, and a drawing that was a circle stays one. Named here
   with the same weight as the blanket, later in the sheet, so they win. */
.spinner,.load-mark,.icon-btn,.mk-email-card a .ic,.ready-sum::before,
.nl-strip .thumb .x,.pm-arrow,.pcard .pdel,.cp-del,.gal-nav,.gal-expand,.switch .track::after,
.bm-terms li::before,.frame-bar i{border-radius:50%!important}
/* `.ready-row .ready-ic` WAS IN THAT LIST, and it is why the dashboard ticks
   were the odd ones out (owner 2026-09-11: "Within the dashboard they're a bit
   curvy, which is wrong ... I like the blocky look"). It does not belong with
   the drawings the exception is for: a spinner and a bullet were always round,
   whereas this is the confirmation mark, and the confirmation mark is a square
   everywhere else. `.ready-sum::before` stays -- that one really is a bullet. */
.gal-scrub-grip,.gal-scrub::before,.switch .track{border-radius:999px!important}
/* The wordmark's box is a quarter taller than its letters (owner 2026-09-11:
   "make sure the size is big enough for the page"): 48px in the bar puts the
   letters at ~36px, 56px in the footer at ~42px, 40px on the auth card at ~30px. */
body.mk .mk-logo{height:48px;width:auto}
body.mk .mk-footer img{height:56px;width:auto}
body.auth .auth-logo{height:40px;width:auto}

/* THE FRAME. A browser window, not a card: hairline, the three window marks
   (squares, since nothing here is round) and the address are what the three
   reference sites share, and they say "this is the product" without a
   caption having to. .frame-slot keeps the recording's own 2236x1264 box, so
   nothing moves when the walkthrough lands in it. */
.frame{position:relative;max-width:1218px;margin:0 auto;border:1px solid var(--line);
  overflow:hidden;background:#fff;box-shadow:0 30px 80px rgba(11,11,12,.12),0 2px 6px rgba(11,11,12,.06)}
.frame-bar{display:flex;align-items:center;gap:8px;height:38px;padding:0 14px;
  background:#FAFAFC;border-bottom:1px solid var(--line)}
.frame-bar i{width:11px;height:11px;background:#CFD3DE}
.frame-bar .url{margin:0 auto;font-size:12px;color:#63637A;background:#fff;
  border:1px solid var(--line);padding:4px 14px;min-width:min(260px,50%);text-align:center}
.frame-slot{position:relative;aspect-ratio:2236/1264;overflow:hidden;background:#fff}
.frame-copy{position:absolute;left:clamp(24px,3.5vw,44px);right:clamp(24px,3.5vw,44px);
  bottom:clamp(24px,3.5vw,44px);z-index:2}
.frame-eyebrow{color:#63637A;font-weight:600;font-size:13px;letter-spacing:.08em;
  text-transform:uppercase;margin:0 0 12px}
.frame-copy h2{font:700 clamp(30px,3.3vw,50px)/.98 var(--display);letter-spacing:-.022em;
  color:#0B0B0C;margin:0 0 12px}
.frame-copy p:last-child{color:#4A4D57;font-size:16px;line-height:1.5;margin:0;max-width:34ch}
/* THE SKELETON (owner: "The page as a skeleton looks nice"). The listing page
   drawn as the blocks a screen shows the moment before it loads -- the
   sidebar, the photo, the fields -- with one slow sheen crossing them. No
   label: the picture already says "not yet". */
.skeleton{position:absolute;inset:0;display:grid;grid-template-columns:17% 1fr;background:#fff}
.skeleton .sk-side{border-right:1px solid var(--line);padding:5% 5%;display:grid;gap:10%;align-content:start}
.skeleton .sk-side i{display:block;height:11px;background:#F4F5FA}
.skeleton .sk-side i:first-child{width:62%;height:16px;background:var(--line)}
.skeleton .sk-main{padding:4% 4% 0;display:grid;grid-template-columns:1fr 1fr;gap:3%;align-content:start}
.skeleton .sk-title{grid-column:1/-1;height:22px;width:22%;background:var(--line);margin-bottom:1%}
.skeleton .sk-card{border:1px solid var(--line);padding:3.5%;display:grid;gap:7%;align-content:start;min-height:0}
.skeleton .sk-card i{display:block;background:#F4F5FA;height:12px}
.skeleton .sk-ph{height:auto;aspect-ratio:398/244;background:#F4F5FA}
.skeleton .sk-row{display:flex;gap:3%}
.skeleton .sk-row i{flex:1;aspect-ratio:1;height:auto}
.skeleton .sk-tall{height:auto;aspect-ratio:5/2}
.skeleton::after{content:"";position:absolute;inset:0;
  background:linear-gradient(100deg,transparent 30%,rgba(255,255,255,.75) 50%,transparent 70%);
  transform:translateX(-100%);animation:sk-sheen 3.2s ease-in-out infinite}
@keyframes sk-sheen{to{transform:translateX(100%)}}
.skeleton-veil{position:absolute;inset:0;z-index:1;
  background:linear-gradient(to top,#fff 22%,rgba(255,255,255,.35) 60%,rgba(255,255,255,0))}
@media (prefers-reduced-motion:reduce){.skeleton::after{animation:none;display:none}}

/* Legal text ran 92ch at 15.5px with no max-width. These are the pages people
   actually have to read closely, and they were the hardest to. 68ch is inside
   the comfortable 60-75 band. */
.legal-band p,.legal-band li{max-width:68ch}
/* A link inside a sentence is underlined (2026-09-11 accessibility sweep, axe
   "link-in-text-block"): the accent against body text is 1.37:1, so colour
   alone did not say "link" to anyone who cannot see the difference. */
.legal-band p a,.legal-band li a,.bm-legal a,.set-d a{text-decoration:underline;text-underline-offset:2px}
/* the backup checkbox reads at the size of the row's own description */
.set-check{display:flex;align-items:center;gap:6px;margin-top:8px;font-size:13px;color:var(--ink-soft)}

/* The 999px leftovers (login tabs and button, the 404 links) are fixed in those
   pages themselves. They are page-local components, and a shared override would
   have been a specificity fight with the rule right next to the markup. */

/* ---- 2FA card ---- */
.sec-panel{margin-top:14px;padding:16px;border:1px solid var(--line);
  border-radius:var(--r-lg)}
.sec-panel .set-t{margin-bottom:10px}
.sec-step{font-size:12px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;
  color:var(--muted);margin:14px 0 8px}
.sec-step:first-child{margin-top:0}
.sec-tiles{display:flex;gap:10px;flex-wrap:wrap}
.sec-tile{flex:1 1 190px;max-width:260px;display:flex;flex-direction:column;gap:3px;
  align-items:flex-start;padding:12px 14px;border:1px solid var(--line);
  border-radius:var(--r-lg);background:var(--card);cursor:pointer;text-align:left}
.sec-tile:hover,.sec-tile:focus-visible{border-color:var(--accent)}
.sec-tile .t{font-weight:600;font-size:14px;color:var(--ink)}
.sec-tile .d{font-size:13px;color:var(--muted)}
/* Sized to hold all six digits standing still. It was 9ch, which with 4px of
   tracking and 28px of padding could not fit them -- the box scrolled as the
   code was typed and the first digits disappeared (owner 2026-08-27). A fixed
   width and tabular figures mean the digits land in the same place every time;
   text-indent cancels the trailing letter-space so they read centred. */
.sec-code{width:196px !important;max-width:100%;letter-spacing:.42em;
  text-indent:.42em;font-size:19px;font-weight:600;text-align:center;
  font-variant-numeric:tabular-nums}
.sec-qr-tile{display:inline-block;padding:10px;background:#fff;
  border:1px solid var(--line);border-radius:var(--r-lg)}
.sec-cancel{display:inline-block;margin-top:12px}
/* The enrolled-methods list. It appears only once a seller holds more than
   one factor -- with a single one the card's toggle is the whole control. */
.sec-methods{display:flex;flex-direction:column;align-items:flex-start;gap:2px;
  margin-top:10px}
.sec-method{display:flex;align-items:center;justify-content:space-between;
  gap:16px;width:100%;max-width:420px;padding:8px 0;
  border-bottom:1px solid var(--line)}
.sec-method:last-of-type{border-bottom:0}
.sec-method .t{font-size:14px;color:var(--ink)}
.sec-add,.sec-off-link{padding:0;border:0;background:none;cursor:pointer;
  font:inherit;font-size:13px;color:var(--muted);text-decoration:underline;
  text-underline-offset:2px}
.sec-add{margin-top:8px}
.sec-add:hover,.sec-off-link:hover,
.sec-add:focus-visible,.sec-off-link:focus-visible{color:var(--ink)}

/* ---- photo tools debug panel (root account only) ----
   Auto-orient answers a seller the same way whether nothing was crooked or the
   identifier was never reached, and the identifier deploys by hand from the
   branch the conductor auto-deploys. This is the admin's view of which. It is
   deliberately plain: it is a readout, not a feature. */
#pt-debug{margin-top:14px}
.ptd{border:1px solid var(--line);border-radius:6px;background:var(--bg);
  font-size:13px;padding:0 12px}
.ptd summary{cursor:pointer;padding:10px 0;font-weight:600;font-size:13px;
  display:flex;align-items:center;gap:10px;list-style:none}
.ptd summary::-webkit-details-marker{display:none}
.ptd summary::before{content:"▸";color:var(--muted);font-size:11px}
.ptd[open] summary::before{content:"▾"}
.ptd-chip{margin-left:auto;font-weight:600;font-size:11px;border-radius:999px;
  padding:2px 8px;background:#E9EAF2;color:var(--muted)}
.ptd-chip.ok{background:var(--success-bg);color:var(--success)}
.ptd-chip.err{background:#FCE8E9;color:var(--danger)}
.ptd-verdict{margin:0 0 10px;line-height:1.5}
.ptd-verdict.err{color:var(--danger)}
.ptd-verdict.muted,.ptd-verdict.ok{color:var(--ink-soft)}
.ptd-facts{display:grid;grid-template-columns:auto 1fr;gap:4px 12px;margin:0 0 10px}
.ptd-facts dt{color:var(--muted);font-weight:600}
.ptd-facts dd{margin:0;color:var(--ink-soft)}
.ptd-name{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;
  word-break:break-all}
.ptd-tbl{width:100%;border-collapse:collapse;margin-bottom:10px}
.ptd-tbl th{text-align:left;font-size:11px;letter-spacing:.06em;
  text-transform:uppercase;color:var(--muted);padding:6px 8px 4px;
  border-bottom:1px solid var(--line)}
.ptd-tbl td{padding:6px 8px;border-bottom:1px solid var(--line);color:var(--ink-soft)}
.ptd-tbl tr:last-child td{border-bottom:0}
.ptd-copy{margin:0 0 12px}

/* The "without" state of a three-state feature row. It is an assertion like
   the ticked state, so it reads as chosen rather than greyed -- but it is the
   opposite assertion, so the mark is a minus and the ink is the warning tone
   rather than the accent. (Autolight SCHEMA 1.6, owner ruling 2026-08-29.) */
.tcp-opt.tcp-absent .tcp-box{color:var(--warn)}
.tcp-opt.tcp-absent .tcp-name{color:var(--warn);font-weight:600}

/* ---- raw model output panel (root account only) ----
   Shares the photo-tools panel's chrome deliberately: two readouts that answer
   "what did the backend actually do", and one visual language for both. */
#model-debug{margin-top:10px}
#model-debug .mdz{color:var(--muted);font-style:italic}
#model-debug .mdw{color:var(--warn);font-weight:600;font-size:12px}
#model-debug .mdp{margin:0 0 8px;color:var(--ink-soft);font-size:13px}
/* The line that says a schema key arrived and nothing here reads it. It is the
   whole reason this panel exists, so it does not whisper. */
#model-debug .mdu{margin:0 0 10px;padding:8px 10px;border-radius:6px;
  background:var(--warn-bg);color:var(--warn);font-size:13px;line-height:1.5}


/* Homepage reveal, replacing motion-13.1.0.js (owner 2026-09-03). Opacity and
   transform only -- both composited, so the reveal costs no layout and no
   main-thread work per frame. The hidden state is applied by anim.js, never by
   the stylesheet, so a page whose script does not run is simply visible. */
.anim-rise{opacity:0;transform:translateY(20px);
  transition:opacity .6s cubic-bezier(.2,.8,.2,1),
             transform .6s cubic-bezier(.2,.8,.2,1)}
.anim-rise.anim-in{opacity:1;transform:none}
@media (prefers-reduced-motion:reduce){
  .anim-rise{opacity:1;transform:none;transition:none}
}

/* Comps: live eBay listings for a part like this one. A scrollable list of
   rows and nothing else -- there is deliberately no summary line, because a
   figure derived from these would be a suggested price.

   DRAWN AS A QUOTATION, NOT AS A CARD. It used to be a 1px box sitting inside
   the listing card's own 1px box, while the two blocks either side of it --
   Product Price above, Item Specifics below -- draw no container at all. Three
   siblings, one of them boxed, reads as an unfinished screen.

   But the container could not simply go: the ALA's Public Display clause wants
   eBay Content visually isolated from ours rather than mixed into it. So the
   isolation is carried by the two devices a quoted block has always used -- a
   recessed ground and a rule down its edge -- which say "this came from
   somewhere else" without claiming to be a second card. The rule is
   --line-strong, a neutral: blue in this app means "this does something", and
   nothing in here is ours to act on.

   Everything inside is on DESIGN.md's spacing ramp (8/14/16/26/40). It was on
   9px and 12px, neither of which is a step on any scale here, inside a card
   whose own padding is 25px. */
.comps-why{margin:6px 0 0;font-size:12px;color:var(--muted)}
.comps-wrap{margin:0 0 8px}

/* THE THUMBNAIL'S OWN BOX. Owner 2026-09-06, settling it: "in card", and the
   same day: "a sidebar that sits on the product thumbnail and is aligned with
   whatever is on the right side of the page."

   It was position:fixed at 50vw on the left, and that had two faults he named
   in one sentence. It lay over the dashboard rail; and the rail is 236px
   expanded and 64px collapsed while 50vw is neither, so the panel's
   relationship to the page changed every time the rail moved. No viewport
   width can agree with a rail that is not a fraction of the viewport.

   Living inside .nl-photos fixes both by not having an opinion. The card is
   already in the main column, so the rail is never beneath it in either state;
   it is already the tall vertical column a stack of photographs wants; and it
   aligns with the right-hand column by construction, being its grid partner --
   which is the alignment he asked for and the thing a fixed drawer could never
   give.

   The card's other children are HIDDEN, not covered: a thumbnail merely
   painted over is still in the tab order, so the seller could tab into buttons
   they cannot see. The card keeps its own sticky/max-height behaviour above
   1050px, so the panel scrolls inside exactly the box the thumbnail occupied.

   There is no scrim. A scrim dims the whole window, dashboard included, which
   is the one thing this panel was told not to cover. */
/* BELOW 1051px ONLY, and that is the whole point. This rule used to be
   unconditional, and `.nl-photos` also carries `position:sticky;top:24px` from
   the min-width:1051px block above. Same specificity, this one later, so
   `relative` won -- and `top:24px` survived, because this rule does not set
   `top`. A sticky element ignores `top` until it is scrolled to; a RELATIVE one
   obeys it immediately. So the photo card sat 24px below the right column at
   rest, and never stuck.

   Measured at 1280px before the fix: card top 722, right column top 698.
   That is the owner's "the product thumbnail is still not aligned", reported
   three times, and it is not a scroll-position problem -- it is 24px at rest,
   which is why two attempts that checked scroll zero found the boxes flush and
   were looking at the wrong thing. The comment above claims "the card keeps its
   own sticky/max-height behaviour above 1050px"; the rule underneath it took
   exactly that away.

   Above 1051px, sticky already establishes the containing block the panel
   needs, so nothing is lost by letting it win. */
@media (max-width:1050px){.nl-photos{position:relative}}
/* ONE SCROLLER, NOT TWO. Owner 2026-09-06: "The scroll bar starts at the
   middle for some reason."

   Above 1050px the CARD is itself a scroller -- position:sticky with
   max-height:calc(100vh - 48px) and overflow:auto, so the photo column can be
   taller than the viewport and still be reachable. The panel then put a second
   scroller inside it. Two nested scrollers means the panel's contents can be
   at the top of their own box while the box is halfway down the card's, which
   is exactly "starts in the middle" -- and resetting the inner one, which is
   what shipped, could never fix it.

   While the panel is up the card does not scroll at all: the panel's own
   .comps-rows is the only scroller, and it fills the card. */
.nl-photos.comps-open{display:flex;flex-direction:column;padding:0;
  overflow:hidden;
  /* BOUNDED AT EVERY WIDTH, AND THIS IS THE FOURTH REPORT OF THE SAME THING.
     Owner 2026-09-07: "The page still appears to me with the scroll bar
     halfway through the list. I have to scroll back up everytime."

     The max-height that makes .comps-rows a scroller lived ONLY in the
     >=1051px block, alongside position:sticky. Below that width the card had
     no height limit at all, so it simply grew to fit every row: .comps-rows
     had overflow-y:auto over an unbounded parent and therefore never
     scrolled, and the WINDOW was the scroller instead. Every fix so far reset
     .comps-rows.scrollTop, which at that width is a no-op on a box that
     cannot move -- three correct fixes to a scroller that was not the one
     carrying the position.

     Stated here rather than in the media query so the panel cannot be given a
     height in one column count and not the other again. Above 1051px the
     sticky rule sets the same value; repeating it costs nothing and removes
     the dependency. */
  max-height:calc(100vh - 48px)}
.nl-photos.comps-open > :not(.comps-panel){display:none}
/* IT FILLS THE ROW, so the animation has a page to play across. Owner
   2026-09-08: "extend the page to align with the bottom of the item specifics
   and have the animation play across a loading page to make it more intuitive
   rather than a blue strip."

   .nl-grid sets align-items:start, so this column was only ever as tall as its
   own content -- and while the fetch is in flight that content is a header
   over an empty list. That is the strip: not a short animation, a short BOX.
   The flex chain beneath it was already right (.comps-panel flex:1 over
   .comps-rows flex:1), and every one of those fills a parent that had no
   height to give.

   align-self on the grid item is the whole fix: the row's height is already
   set by the item specifics beside it, so stretching into it aligns the two
   columns' bottoms by construction rather than by measuring either.

   SCOPED TO THE LOADING STATE, and that is not caution for its own sake.
   Above 1050px .nl-photos is position:sticky, and a grid item stretched to
   fill its row has no travel left to stick with -- the finished panel would
   stop following the seller down the page. That is a behaviour he did not ask
   to change: the ask was for the LOADING page to be a page. So the stretch
   arrives with the work and leaves with it.

   The floor is for the one case stretching cannot cover -- a part whose
   specifics column is itself short, where matching it would still be a strip.
   Loading is the only state that takes it, so a finished panel with two rows
   is still two rows tall. */
.nl-photos.comps-open.is-loading{align-self:stretch}
.comps-rows.is-working{min-height:min(42vh,340px)}
.comps-panel{display:flex;flex-direction:column;min-height:0;
  flex:1 1 auto;background:var(--card);border-radius:inherit}
/* The head is the card's own heading now, not a note bar bolted above a list.
   Sticky, so however far down the photographs the seller has gone they can
   still see what they are looking at and still get out. */
/* THE WHOLE PANEL TINTS WHILE IT WORKS. Owner 2026-09-08: "make the show
   listings page the same blue tint animation effect like within the demo. The
   whole page should undergo that animation."

   The demo pulses the Lotwize mark's tiles between .05 and .11 opacity on
   #2563EB. THE SAME NUMBERS AND THE SAME CURVE are used here, over the panel
   rather than over a logo -- if the demo's timing is ever retuned, these
   should move with it, which is why they are stated as its values and not as
   fresh ones that happen to look similar.

   An overlay, not a background: the rows underneath carry photographs and
   their own white cards, and tinting the panel's background would leave every
   one of them a pale island in it. inset:0 over the top tints all of it at
   once, which is what "the whole page" asks for.

   pointer-events:none so a seller can still scroll and press through it, and
   the panel is only ever in this state while a fetch is in flight. */
/* THE TINT SITS UNDER THE HEADER, NOT OVER IT. Owner 2026-09-08: "when a user
   clicks 'show listings', the whole page placeholder should open and the blue
   loading tint should only be applied on the whitespace of the page. Under
   'show listings' header".

   It was on the panel at inset:0, so it washed over the heading, the
   explainer and the close control -- the three things that are already
   finished and are what makes the opened panel a placeholder rather than a
   blank. Moved to .comps-rows, which is the flex child that takes the
   remaining height: while a fetch is in flight the rows are empty, so that
   box IS the whitespace under the header, exactly and without measuring
   anything.

   .comps-panel keeps position:relative -- .comps-mine and the close button
   are positioned against it. */
.comps-panel{position:relative}
/* BOTTOM CORNERS ONLY. Owner 2026-09-08: "the border between the blue loading
   page and the top, there are little corners on each side and should be flush
   aligned with the bar instead of the divot."

   The tint fills the rows box, which starts immediately under the sticky
   header. A radius on all four corners curved the top two away from the
   header's bottom edge, leaving a lit notch at each end of an edge that is
   meant to read as one line. The bottom two still meet the card's own corner
   and keep theirs.

   THE LITERAL FORM, not 0 0 var(--r) var(--r), which tests/test_type_system.py
   correctly rejected: the radius scale admits directional radii only as the
   px steps (0 0 6px 6px is already used in three places), and --r is 6px, so
   this is the same value written the way the file writes it. Matching the
   convention beats widening the guard that caught it. */
.comps-rows.is-working::after{content:"";position:absolute;inset:0;top:var(--mine-h,0);
  pointer-events:none;z-index:3;border-radius:0 0 6px 6px;
  background:#2563EB;animation:comps-tint 2.4s ease-in-out infinite}
@keyframes comps-tint{0%,60%,100%{opacity:.05}30%{opacity:.11}}
@media (prefers-reduced-motion:reduce){
  .comps-rows.is-working::after{animation:none;opacity:.07}}

/* TOP CORNERS ONLY. Owner 2026-09-09: "There is a little seam that the show
   listings page makes when loading ... it appears on both sides of where it
   connects to the blue tinted placeholder screen."

   `border-radius: inherit` copied all FOUR corners from the card, so the head's
   BOTTOM corners were rounded too -- and the tint below it is square across the
   top (0 0 6px 6px). The head curved away from the tint at each end and the
   card showed through: two small notches, one per side, exactly where he says.
   Nothing was misaligned; the two edges were different shapes. */
.comps-head{position:sticky;top:0;z-index:2;display:flex;align-items:flex-start;
  gap:16px;padding:25px 25px 14px;background:var(--card);
  border-bottom:1px solid var(--line);border-radius:6px 6px 0 0}
.comps-head-txt{flex:1 1 auto;min-width:0}
.comps-h{margin:0;font-size:19px;line-height:1.3}
.comps-note{margin:6px 0 0;font-size:13px;line-height:1.5;color:var(--ink-soft)}
/* Beside the X in the sticky head. flex:none so a long label cannot squeeze
   the heading, and the row is baseline-aligned by .comps-head already. */
/* THE MATCH ACTION, on its own line above the rows it acts on rather than
   crowded beside the X in the sticky head. Full width so the label can say
   what the button does instead of being trimmed to fit a corner. */
/* The chosen row. A ring rather than a fill: the row already carries a photo
   and a price, and tinting it would read as a status on the LISTING. */
.comps-close{flex:none;width:32px;height:32px;border:0;border-radius:var(--r);
  background:none;color:var(--muted);font-size:22px;line-height:1;
  cursor:pointer;transition:background 150ms ease-out,color 150ms ease-out}
.comps-close:hover{background:var(--bg);color:var(--ink)}
.comps-close:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
/* Fills the card and scrolls; the form beside it does not move. */
/* The seller's OWN listings for this part, above the market rows. New class
   names only, so nothing here can override a comps rule. */
/* HIS OWN LISTINGS SEPARATE BY TONE, NOT BY ANOTHER BORDER. They used the
   same row styling as the market rows below and were told apart only by a
   heading and a hairline, so the two blurred into one list -- and the two
   answer different questions: what HE already has for this part, against what
   other people are asking for it.

   Recessed onto --bg against the panel's --card, which is the light theme's
   version of the tonal step the design system asks for (Graphite -> Slate ->
   Field Fill) rather than a second box. No shadow, no card-in-card. */
.comps-mine{background:var(--bg);border-bottom:1px solid var(--line);
  border-radius:var(--r);padding:10px 12px;margin:0 0 10px}
.comps-mine-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px}
.comps-mine-h{font-size:13px;margin:0 0 6px;letter-spacing:.02em}
.comps-mine-note{margin:6px 0 0;font-size:12px}
/* One of his own, in the same row as the market (owner 2026-09-10: "displayed
   just like the other comps but just have a disclaimer that it's one of
   theirs"). */
.comp-mine-note{font-size:12px;color:var(--muted);margin-top:6px}
/* His rows sit first inside the same scroller as the market's (owner
   2026-09-10). A hairline tint tells them apart without breaking the list. */
/* Own rows wear the market palette, letterbox included: the gaps either
   side of a photo are the same shade as a pulled comp's (owner 2026-09-11,
   "Make sure the tint is the same shade for the active / sold listings as
   the pulled comps"), so .gal-stage's own background stands. */
.comps-mine-rows .comp-row-mine{background:transparent}
.comps-mine-head{font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);padding:10px 0 6px}
.comps-mine-head span{font-weight:500;letter-spacing:0;text-transform:none;margin-left:6px}
.gal-empty{display:flex;align-items:center;justify-content:center;aspect-ratio:8/3;background:var(--bg);color:var(--muted);font-size:13px;padding:12px;text-align:center}
.comp-mine-warn{font-size:12px;padding:8px 16px}
.comps-debug{margin:8px 16px 16px;font-size:12px;color:var(--muted)}
.comps-debug summary{cursor:pointer;font-weight:600;color:var(--ink)}
.comps-debug table{width:100%;border-collapse:collapse;margin-top:6px}
.comps-debug td{padding:3px 6px 3px 0;vertical-align:top;border-top:1px solid var(--line)}
.comps-debug td:last-child{white-space:nowrap;color:var(--ink)}
.comps-debug .cd-stage{margin:8px 0 2px;font-weight:600;color:var(--ink)}

.comps-rows{position:relative;flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain;
  padding:0 25px 25px}
.comps-empty{padding:0 25px 25px}
/* 16px, the card scale (DESIGN.md 8/14/16/26/40). The photo is what
   makes the row feel roomy; the inset does not have to. */
.comp-row{border-bottom:1px solid var(--line);padding:16px}
.comp-row:last-child{border-bottom:0}

/* THE PHOTO IS THE ROW. Everything else is a caption under it: title, price,
   then the seller's own words. Four facts, in the order somebody comparing
   actually wants them -- what does it look like, what is it, what are they
   asking, what is wrong with it. */
.comp-shot{margin:0 0 12px}
/* HALF THE HEIGHT. Owner 2026-09-06: "Cut down the picture size by half."
   Full width at 4/3 is 0.75 of the panel tall, and fifteen of those is a
   column nobody reaches the end of -- the same complaint behind the one-line
   condition note and the scroll-to-top.
   8/3 is exactly half of 4/3, so the stage is half as tall. The photograph is
   unchanged and undistorted: the stage is `contain`, so a landscape shot
   renders smaller and centred rather than being cropped or stretched to a
   ratio no headlight photo has. */
.comp-shot .gal-stage{aspect-ratio:8/3;border-radius:var(--r)}
.comp-line{display:block;padding:0;color:inherit;text-decoration:none}
.comp-line:hover .comp-title{text-decoration:underline}
.comp-line:focus-visible{outline:2px solid var(--accent);outline-offset:3px;
  border-radius:var(--r)}
.comp-title{display:block;font-size:14px;line-height:1.45;font-weight:600}
.comp-price{display:block;margin-top:4px;font-size:19px;font-weight:700;
  font-variant-numeric:tabular-nums}

/* The seller's own words about what is wrong with the part, fetched for this
   row only. Inside the eBay panel and visually part of it: the ALA's Public
   Display clause requires eBay Content to be isolated from ours rather than
   mixed into it. */
.comp-more{display:block;margin:0 0 14px;padding:0;border:0;background:none;
  font:inherit;font-size:13px;color:var(--accent);cursor:pointer}
.comp-more:hover{text-decoration:underline}
.comp-more[aria-expanded="true"]{font-weight:600}
/* Figure and ground swap: white on the panel's recessed ground, where the rows
   are the ground and this is the inset. */
.comp-notes{margin:0;padding:14px 16px;border-radius:var(--r);
  background:var(--card);font-size:13px;line-height:1.5;color:var(--ink-soft)}
.comp-notes-none{color:var(--muted);font-style:italic}
/* A row that came down already read: its pictures and the seller's words about
   them, side by side where there is room (owner 2026-09-05).

   Together because they answer halves of one question -- the photo says what
   forty dollars looks like, the notes say what is wrong with it -- and the
   button they replace made you hold one in your head while you went for the
   other. Stacked below 760px, where two columns would leave the gallery too
   narrow to read a lens scratch in.

   Still eBay Content, still inside the eBay panel: the ALA's Public Display
   clause asks for it to be isolated from ours, not blended into the page. */
/* ALIGNMENT. Owner 2026-09-06: "Fix all alignment issues for the show listings
   page."

   Three things in one row were inset differently, and every one of them was a
   leftover from the side-by-side layout that .comp-detail used to describe --
   a layout compsRow has not emitted since the rows became photo-first, so the
   rules were dead as a design and alive as a bug.

   .comp-row already pads 16px. On top of that the photo carried margin-left
   16px and a 260px cap, and the notes and the button carried margin 16px of
   their own -- so the picture started 32px in and stopped short of the right
   edge, the notes sat 32px in, and the title and price sat at 16px. Four
   different left edges in one row.

   One inset now, owned by .comp-row, and everything inside it starts at the
   same place. */
.comp-gal{display:block;max-width:none;margin:0}
/* A REAL SCROLLBAR ON THE THUMBNAILS. Owner 2026-09-07: "Add the ability to
   scroll through the photos using a horizontal scroll bar within the comps."

   The strip has always been overflow-x:auto, so it COULD be dragged -- but
   with an overlay scrollbar (every modern browser, unless a mouse is plugged
   in) there is nothing on screen saying so, and a row of thumbnails that
   stops at the panel edge reads as all the photos there are. The bar is what
   makes the rest of them discoverable, so it is asked for explicitly rather
   than left to the platform.

   Snap so a drag settles on a thumbnail rather than halfway across one. */
/* A REAL SCROLLBAR ON EVERY STRIP THAT SCROLLS. Owner 2026-09-07 for the comps
   photos, and again 2026-09-08: "Add a scroll bar at the bottom of the product
   thumbnail photos, as well as within the comps in show listings page."

   These have always been overflow:auto, so they COULD be dragged -- but every
   modern browser draws an overlay scrollbar that is invisible until you are
   already scrolling, unless a mouse is plugged in. A row of thumbnails that
   stops at the panel edge reads as all the photos there are, and a tall list
   with no bar reads as a short one. The bar is what makes the rest of it
   discoverable, which is why it is asked for explicitly rather than left to
   the platform.

   ONE RULE FOR ALL THREE, not three copies. It was written for the comps
   gallery and then wanted on the seller's own strip, and a second copy is how
   the two come to differ by a colour nobody chose. */
.nl-strip,
.comp-gal .gal-strip{scrollbar-width:thin;scrollbar-color:var(--line) transparent}
/* TWICE AS WIDE on the panel's own scroller (owner 2026-09-10: "Make the
   scroll bar slightly wider, maybe 2x as big"). It is the bar a seller drags
   through fifteen photographed rows; the thumbnail strips keep the thin one. */
.comps-rows{scrollbar-width:auto;scrollbar-color:var(--line) transparent}
.nl-strip::-webkit-scrollbar,
.comp-gal .gal-strip::-webkit-scrollbar{height:8px}
/* The panel's own scroller is vertical, so the same bar is a WIDTH here. */
.comps-rows::-webkit-scrollbar{width:16px}
.nl-strip::-webkit-scrollbar-track,
.comp-gal .gal-strip::-webkit-scrollbar-track,
.comps-rows::-webkit-scrollbar-track{background:transparent}
.nl-strip::-webkit-scrollbar-thumb,
.comp-gal .gal-strip::-webkit-scrollbar-thumb,
.comps-rows::-webkit-scrollbar-thumb{background:var(--line);border-radius:999px}
.nl-strip::-webkit-scrollbar-thumb:hover,
.comp-gal .gal-strip::-webkit-scrollbar-thumb:hover,
.comps-rows::-webkit-scrollbar-thumb:hover{background:var(--muted)}

/* Snap so a drag settles on a thumbnail rather than halfway across one. Only
   the horizontal photo strips: snapping a list of whole comp rows would fight
   a seller reading down it. */
.comp-gal .gal-strip,
.nl-strip{scroll-snap-type:x proximity}
.comp-gal .gal-thumb,
.nl-strip .thumb{scroll-snap-align:start}

/* THE SHOW-LISTINGS BAR. Owner 2026-09-07: "the status bar is too small within
   the show similar", then "Do not use the blue status bar above the
   identification, just make another one appear on top of it for clarity."

   It inherits .id-banner whole -- same tint, same padding, same type -- so the
   two bars are the same object saying different things, which is the point of
   stacking them. Only the gap between them is new: without it two banners with
   the same background read as one tall bar with two paragraphs in it. */
.comps-status{margin-bottom:12px}
.comps-status .idb-txt .muted{font-size:13px;line-height:1.45;margin-top:2px}
/* The failure state. Not red: nothing is broken for the SELLER -- their
   listing is fine and the price was always theirs to set -- so this is the
   same warn amber the rest of the app uses for "this did not happen", not an
   error colour that implies their work is at risk. */
.comps-status.warn{background:var(--warn-bg)}
.comps-status.warn .idb-txt b{color:var(--warn)}
/* The "nothing found" line lives in the same box as the rows, so it takes
   their inset (owner 2026-09-05) -- and enough height that an empty panel is a
   considered answer rather than a sliver. It also carries the honest failure
   sentence when the lookup broke, which runs to two lines. */
#comps-empty{margin:0;padding:26px 16px}
.comp-line:hover{background:var(--tint,#f6f8fb)}
/* Inset, not the global +2px: the row runs edge to edge, so an outward ring
   is clipped by the panel. --focus was never defined anywhere, so this drew
   its literal fallback -- a blue that is not the app's. */
.comp-line:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
/* The one place this panel raises its voice. The seller is scanning a column
   of prices and reading the titles only when one stops them, so the price is a
   step up the ramp from the row it sits in and the title a shade softer. */
.comp-price{font-size:16px;font-weight:700;font-variant-numeric:tabular-nums;
  white-space:nowrap}
.comp-cond{color:var(--muted);font-size:13px;white-space:nowrap;
  overflow:hidden;text-overflow:ellipsis}
.comp-title{color:var(--ink-soft);overflow:hidden;text-overflow:ellipsis;
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
@media (max-width:720px){
  .comp-line{grid-template-columns:84px 1fr;column-gap:16px;row-gap:8px}
  .comp-cond{grid-column:2}
  .comp-title{grid-column:1 / -1}
}


/* ============ the loading ring ============
   Every wait a seller sits through on the way in draws this: pressing Log In,
   pressing Create account, and answering a 2FA code. It lived in login.html
   until 2026-09-04, which is why the other two screens had nothing turning.

   A ring rather than the brand mark (owner 2026-09-04, when the mark was
   spinning here): rotating a logo makes the identity look like the thing that
   is loading, and ours is three squares whose gaps strobe as they turn. A
   circle has no orientation to lose.

   Drawn in CSS, not fetched. It is a 3px ring, and an image request for it
   would be one more thing to wait for on the screen whose whole job is
   waiting. */
.load-mark{height:19px;width:19px;display:inline-block;flex:none;
  border-radius:50%;border:2.5px solid var(--line,#e6e7ee);
  border-top-color:var(--accent,#2563eb);vertical-align:-4px;
  animation:lz-spin .85s linear infinite}
@keyframes lz-spin{to{transform:rotate(360deg)}}
@keyframes lz-breath{0%,100%{opacity:.45}50%{opacity:1}}
/* No spin, and the whole ring takes the accent: a stationary ring with one
   coloured arc reads as a progress value stuck at 12%, which is a worse lie
   than saying nothing. Breathing says "working" without motion across the
   screen. */
@media (prefers-reduced-motion:reduce){
  .load-mark{border-color:var(--accent,#2563eb);
    animation:lz-breath 1.8s ease-in-out infinite}}
/* On a filled button the grey ring is invisible, so it inverts. Smaller too:
   beside 15px button text a 19px ring is the loudest thing on the screen. */
.auth-btn .load-mark,.btn-pill .load-mark{height:15px;width:15px;
  border-width:2px;border-color:rgba(255,255,255,.45);border-top-color:#fff;
  margin-right:9px;vertical-align:-2px}
@media (prefers-reduced-motion:reduce){
  .auth-btn .load-mark,.btn-pill .load-mark{border-color:#fff}}
/* On the account-deletion panel the grey ring reads as a greyed-out control
   rather than as something working: --line against white is barely a shape,
   and only the arc carries any colour. Owner 2026-09-05 asked for it blue.
   Whole ring in accent at low alpha, arc solid, so it reads as one blue object
   turning -- and bigger, because here the ring IS the loading state rather
   than a mark beside a word. */
.dz-prog .load-mark{height:22px;width:22px;border-width:2.5px;
  border-color:rgba(37,99,235,.22);border-top-color:var(--accent)}
@media (prefers-reduced-motion:reduce){
  /* Same trade as the base rule: no rotation, whole ring in accent, breathing.
     The breath is inherited; only the ring colour needs restating. */
  .dz-prog .load-mark{border-color:var(--accent)}}


/* ============ the photo viewer ============
   Beside the condition notes, and inside an opened live listing. The stage
   holds its shape whether or not an image has loaded -- a panel that snaps to
   height when a photo arrives moves the words underneath it, which on the
   condition box means moving the cursor out from under the seller. */
.gal{display:block;margin:0 0 14px}
.gal:focus-visible{outline:2px solid var(--accent);outline-offset:3px;border-radius:6px}
.gal-stage{position:relative;aspect-ratio:4/3;border-radius:6px;overflow:hidden;
  background:var(--bg);display:flex;align-items:center;justify-content:center}
.gal-img{max-width:100%;max-height:100%;object-fit:contain;display:block}
.gal-nav{position:absolute;top:50%;transform:translateY(-50%);width:34px;
  height:34px;border-radius:50%;border:0;cursor:pointer;font-size:22px;
  line-height:1;color:var(--ink);background:rgba(255,255,255,.86);
  box-shadow:0 1px 4px rgba(16,17,21,.18);display:grid;place-items:center}
.gal-nav:hover{background:#fff}
/* 10px, the same inset the count and the scrubber use. They were at 8 and
   everything else at 10, which is not a gap anyone measures but is exactly the
   "not aligned within the box" a reader sees without being able to name it. */
.gal-prev{left:10px}
.gal-next{right:10px}
/* THE SCRUBBER, OVER THE BOTTOM-CENTRE OF THE PHOTO. Owner 2026-09-08: "one
   where it appears on the bottom center part of the photo (like an overlay),
   you scroll and the pictures are scrolling too. Instead of clicking to
   enlarge, scrolling gets you faster to where you need to go."

   IT IS A REAL SCROLLER WEARING A CONTROL'S CLOTHES. The element genuinely
   scrolls -- which is what makes the wheel, a trackpad swipe and a touch drag
   all work without a line of gesture code -- and the native bar is hidden so
   what shows is a control on the photograph rather than a browser artefact
   sitting on top of it.

   Centred and inset from the edge so it reads as belonging to the image;
   translucent dark so it holds against a bright headlight photo and a dark
   one alike, which a light pill would not. */
/* A SHORT BAR, CENTRED (owner 2026-09-10: "should be a lot smaller in
   length", superseding 2026-09-09's full width). Five photos do not need a
   bar the width of the picture; a short one under the centre reads as a
   position indicator rather than a second frame round the photo.

   A FIXED GRIP ON A DRAWN TRACK. It was the browser's own scrollbar over an
   n-screen scroller, whose thumb is 1/n of the bar by definition and which
   chose the last photo before the thumb reached the end. The grip is one
   size whatever the count; mountScrubber (app.js) places it by arithmetic,
   follows the pointer continuously while it is held, and eases it onto the
   photo's slot when it is let go.

   The track is 5px of bar inside a 20px hit area, so it is slim to look at
   and easy to catch. Translucent dark so it holds on a bright headlight photo
   and a dark one alike; the grip is the one white thing on the picture. */
.gal-scrub{position:absolute;left:50%;bottom:10px;width:min(44%,168px);height:20px;
  transform:translateX(-50%);cursor:pointer;touch-action:none;
  user-select:none;-webkit-user-select:none}
.gal-scrub::before{content:"";position:absolute;left:0;right:0;top:8px;height:5px;
  border-radius:999px;background:rgba(20,23,28,.45);backdrop-filter:blur(3px);
  transition:background .15s}
.gal-scrub:hover::before{background:rgba(20,23,28,.6)}
.gal-scrub-grip{position:absolute;left:0;top:5px;width:24px;height:10px;
  border-radius:999px;background:rgba(255,255,255,.92);
  box-shadow:0 1px 3px rgba(16,17,21,.35);cursor:grab;
  transition:transform .18s cubic-bezier(.2,.8,.2,1),background .15s}
.gal-scrub:hover .gal-scrub-grip{background:#fff}
.gal-scrub.dragging{cursor:grabbing}
/* No easing under a finger: the grip is where the pointer is, not on its way. */
.gal-scrub.dragging .gal-scrub-grip{cursor:grabbing;transition:none}
.gal-scrub[hidden]{display:none}
@media (prefers-reduced-motion:reduce){.gal-scrub-grip{transition:none}}

/* ABOVE the bar now that the bar reaches this corner. At bottom:8px the two
   overlapped, and the count is the thing that loses -- it is read once, the
   bar is dragged. */
/* Top-right of the stage, opposite the count and clear of both arrows. Same
   plate as .gal-nav so the three overlays on a photo read as one family. */
.gal-expand{position:absolute;top:10px;right:10px;width:30px;height:30px;
  border-radius:50%;border:0;cursor:pointer;color:var(--ink);
  background:rgba(255,255,255,.86);box-shadow:0 1px 4px rgba(16,17,21,.18);
  display:grid;place-items:center;padding:0}
.gal-expand:hover{background:#fff}
.gal-expand svg{width:15px;height:15px}
/* The expanded gallery fills the dialog the way the expanded description does,
   so the two faces of "make this bigger" behave the same way. */
.gal-big-inner{flex:1 1 auto;min-height:0;display:flex;flex-direction:column}
.gal-big-inner .gal-stage{flex:1 1 auto;aspect-ratio:auto;min-height:0}
.gal-count{position:absolute;right:10px;bottom:34px;font-size:12px;
  font-variant-numeric:tabular-nums;color:#fff;background:rgba(16,17,21,.62);
  padding:2px 8px;border-radius:999px}
.gal-strip{display:flex;gap:8px;overflow-x:auto;margin-top:8px;padding-bottom:2px}
.gal-thumb{flex:none;width:56px;height:56px;border-radius:6px;cursor:pointer;
  border:2px solid transparent;background:var(--bg) center/cover no-repeat;
  padding:0}
.gal-thumb.on{border-color:var(--accent)}
.gal-thumb:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
/* The opened row. It spans the card rather than sitting in a column, because
   a gallery squeezed into a grid cell is a thumbnail with arrows on it. */
.lst-open{grid-column:1 / -1;margin-top:10px;padding-top:12px;
  border-top:1px solid var(--line)}
.lst-gal{max-width:360px}
.lst-facts{display:grid;grid-template-columns:auto 1fr;gap:4px 14px;margin:0 0 10px;
  font-size:14px}
.lst-facts dt{color:var(--muted)}
.lst-facts dd{margin:0;color:var(--ink);font-variant-numeric:tabular-nums}
.lst-notes{font-size:14px;line-height:1.55;color:var(--ink-soft);
  white-space:pre-wrap}

/* ============ the wait between text messages ============
   Thirty seconds, enforced server-side; this only draws it (owner
   2026-09-05). One definition in the shared sheet because login, the
   onboarding wizard and the app all send codes and all load this file.

   The bar EMPTIES rather than fills: a stripe that shrinks toward nothing
   reads as time running out, which is what it is, and it ends at zero width
   exactly when the button comes back. */
/* A link that is a button. Inline in login.html until 2026-09-05, when the
   2FA sign-in moved to /reset (owner) and a second page needed it -- the same
   reason the load ring moved here when signup drew it too. login.html keeps
   only the rules that are its own, like hiding the code-step links. */
.link-btn{background:none;border:0;padding:6px 0 0;font:inherit;font-size:13px;font-weight:600;
  color:#357CFF;cursor:pointer;text-decoration:underline;text-underline-offset:3px}
.link-btn[disabled]{color:#9A9AA8;cursor:default;text-decoration:none}
/* The alternative way in, aligned under the row it belongs beside. */
.auth-alt{margin:2px 0 0;text-align:right}
.auth-alt .link-btn{padding:0;font-size:13px}
/* The cooldown in words, beside the code box. Owner 2026-09-05: "Within every
   SMS that this website sends, it has to have a countdown of the current
   cooldown timer near the box."

   Quiet: it is a fact about waiting, not a problem, and it sits directly under
   a field the seller is mid-way through typing into. Tabular numerals so the
   line does not twitch as the seconds tick down -- at 250ms a reflowing digit
   is visible, and a shifting sentence under a code box reads as an error
   appearing. */
.sms-count{margin:6px 0 0;font-size:13px;line-height:19px;color:var(--muted);
  font-variant-numeric:tabular-nums}
.sms-wait{display:block;height:3px;border-radius:999px;background:var(--tint);
  overflow:hidden;margin:8px 0 0}
.sms-wait i{display:block;height:100%;width:100%;border-radius:999px;
  background:var(--accent);transform-origin:left center;
  transition:transform linear}
@media (prefers-reduced-motion:reduce){
  /* No sweep, but the bar still SAYS the button is waiting -- a disabled
     control with no reason is the thing this exists to avoid. */
  .sms-wait i{transition:none}
}

/* ============ change your password ============
   Under Support & Safety beside 2FA, because it spends a factor and they are
   the same subject. The code box matches the ones the sign-in screens use so a
   seller meets the same control in both places. */
#pw-change .set-txt,#em-change .set-txt{max-width:none}
/* The email card (2026-09-10) is the same form with a different second field,
   so it wears the same rules rather than a copy of them. */
#pwc-panel,#emc-panel{margin-top:12px;max-width:340px}
#pwc-panel .field,#emc-panel .field{margin:0 0 12px}
#pwc-panel label,#emc-panel label{display:block;font-size:14px;font-weight:600;margin:0 0 6px}
.pwc-methods{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:12px}
/* The SMS wait, told in words beside the code box.
   Muted while it counts, accent once it is over -- and the WORDS change with
   it ("in 12s" -> "now"), because colour alone is WCAG 1.4.1 and says nothing
   to a seller who cannot see it. */
.sms-note{margin:6px 0 0;font-size:13px;line-height:1.5;color:var(--muted);
  font-variant-numeric:tabular-nums}
.sms-note.is-ready{color:var(--accent);font-weight:600}

/* TRACKED CODE INPUTS COMPENSATE THEIR TRAILING GAP.
   letter-spacing adds its space after EVERY character including the last, so a
   six-digit code carries a trailing gap nobody sees. text-align:center then
   centres the string plus that gap and the visible digits sit half a
   letter-space to the left -- about 4px at .4em on 19px, which reads as a box
   whose contents have slipped rather than as anything anyone can name.
   text-indent of the same value gives the leading edge back what the trailing
   edge already took.
   Six rules across this site set a large tracking on a centred input. On
   2026-09-06 three compensated and three did not, and the three that did used
   two different techniques -- so tests/test_centred_code_inputs.py now checks
   the PROPERTY on every one of them, whichever technique its author picked. */
.pwc-code{max-width:170px;letter-spacing:.4em;text-indent:.4em;text-align:center;font-size:19px;
  font-weight:600}
#pwc-msg.err,#emc-msg.err{color:var(--danger-ink)}
/* The "add it" link runs inside a sentence, so it takes no top padding. */
#emc-other .link-btn{padding:0;vertical-align:baseline}

/* ============ how you pay ============
   The billing-mode card. The terms of the monthly arrangement sit inside it,
   beside the control that agrees to them, so they are styled as body text a
   person reads rather than as fine print they scroll past -- that is the whole
   difference between a disclosure and a formality. */
.bm-terms{list-style:none;margin:14px 0 0;padding:0;display:grid;gap:10px;
  max-width:68ch}
.bm-terms li{position:relative;padding-left:18px;font-size:14px;line-height:1.55;
  color:var(--ink-soft)}
.bm-terms li::before{content:"";position:absolute;left:2px;top:9px;width:5px;
  height:5px;border-radius:50%;background:var(--line-strong)}
.bm-terms b{color:var(--ink)}
/* THE ANSWER FIRST. One sentence carrying what the page is opened to learn --
   how much of the allowance is gone and when the bill lands. The seven rows
   below are the detail behind it, not the headline. */
.bm-lead{margin:0;max-width:60ch;font-size:16px;line-height:24px;
  color:var(--ink);font-variant-numeric:tabular-nums}

/* THE ALLOWANCE AS A POSITION. The TRACK IS THE ALLOWANCE, so a full bar means
   it is spent and "past your allowance" needs no second scale.

   Neutral at every width. Going over is ordinary billing -- the seller pays the
   per-headlight rate and keeps working -- so an amber or red fill would report
   a fault where there is none. The number below says the amount; the bar only
   says how far along. Two tones of the existing ramp, no new hue, and nothing
   that spends the one signal blue on a thing you cannot act on. */
.bm-meter{margin:10px 0 0;height:4px;border-radius:999px;
  background:var(--line);overflow:hidden}
.bm-meter-fill{height:100%;width:0;border-radius:999px;
  background:var(--ink-soft);transition:width .22s cubic-bezier(.2,.8,.2,1)}
@media (prefers-reduced-motion:reduce){.bm-meter-fill{transition:none}}

/* .bm-band is gone with the three headed lists it separated. The page has one
   heading system now -- .sec-head, the app's own -- because two systems on one
   page is the "confusing layout" complaint at its root. */

/* No max-width any more: these sit in the 320px side rail, which is the
   measure. Capping them again would leave a gap inside their own card. */
.bm-lines{list-style:none;margin:0;padding:0;display:grid;gap:0}
.bm-lines li{display:flex;justify-content:space-between;align-items:baseline;
  gap:16px;border-top:1px solid var(--line);padding:10px 0;font-size:14px;
  color:var(--ink-soft)}
.bm-lines b{color:var(--ink);font-variant-numeric:tabular-nums}
.bm-acts,.bm-cap-row{margin-top:16px;display:flex;flex-wrap:wrap;gap:12px;
  align-items:center}
.bm-cap-row label{font-size:14px;font-weight:600;color:var(--ink)}
.bm-cap-row .topup-custom{display:inline-flex}
/* ---- what you are on ---- */
/* The name and its terms left, the price right: the money is what the eye
   goes for, and it should not have to win a line against the name. */
.bm-now-head{display:flex;align-items:flex-start;justify-content:space-between;
  gap:16px}
.bm-now-name{margin:0;font-size:19px;font-weight:700;letter-spacing:-.01em}
.bm-now-terms{margin:4px 0 0;font-size:13px;color:var(--muted);max-width:44ch}
.bm-now-price{margin:0;font-size:26px;font-weight:700;letter-spacing:-.02em;
  white-space:nowrap;font-variant-numeric:tabular-nums}
.bm-comped{margin:0 0 16px}
.bm-choose-p{margin:0;font-size:14px;color:var(--ink-soft);max-width:60ch}

/* The three figures ACROSS. .stat-strip is the dashboard's, shared rather than
   reminted, so both pages read a metric row the same way. */
.bm-figs{margin-top:16px}

/* Cancelling ends the arrangement, so it is not a sibling of "Save a card".
   Tone, not an alarm: it is a legitimate thing to want, and a red box would
   argue with the seller about it. */
.bm-danger{border-color:var(--line-strong)}
.bm-danger-go{color:var(--danger)}
/* .bm-terms is already a list style further up this file. This is the
   paragraph under the columns, and it needed its own name rather than to
   inherit bullets from a rule written for something else. */
.bm-legal{margin:16px 0 0;max-width:80ch}

/* The description preview sits in the textarea's place, at the textarea's
   size, so switching between them does not move the page under the seller. */
/* THE OUTLINE IS THE ONE EVERY OTHER FIELD USES. Owner asked for a black
   outline (2026-09-09) and then, seeing it: "The black outline should match the
   subtleness of the others." So it is var(--field), the same border .input
   carries -- the box reads as a field on this card rather than as the one thing
   drawn in a heavier pen. --line was the original and was too faint: it is the
   hairline used BETWEEN rows inside a card, and against white it all but
   disappears on a box the seller now sees first.

   210px against .tmpl-box's 190px is the "small increase in length that it
   undergoes vertically" -- the preview was 180px and actually stood SHORTER
   than the editor it replaces, so the box shrank on the way into the mode that
   is now the default. */
.desc-preview{display:block;width:100%;min-height:210px;border:1px solid var(--field);
  border-radius:var(--r);background:#fff}
.desc-preview[hidden]{display:none}
/* ---- the description editor (mountRichText) --------------------------------

   Owner 2026-09-09, against a screenshot of eBay's own toolbar. One surface,
   always showing what the buyer will see, edited in place; the checkbox at the
   end of the toolbar reveals the source. Nothing here says "preview" because
   there is no longer a mode to be in.

   THE WHITE GROUND IS DELIBERATE and matches .desc-preview above: a buyer sees
   this description on eBay's white page, not on our dark theme, so showing it
   on our own ground would be showing them something they will not get. It is
   the one surface in the app that does not follow the theme. */
.rt{border:1px solid var(--field);border-radius:var(--r);overflow:hidden;
  background:#fff}
.rt:focus-within{border-color:var(--accent)}
.rt-bar{display:flex;flex-wrap:wrap;align-items:center;gap:4px;padding:6px 8px;
  background:var(--bg);border-bottom:1px solid var(--line)}
.rt-sel{height:28px;border:1px solid #C9CCD6;border-radius:4px;background:#fff;
  color:#101115;font-size:12px;padding:0 4px;max-width:132px}
.rt-sel-sm{max-width:64px}
.rt-btn{display:inline-flex;align-items:center;justify-content:center;
  width:28px;height:28px;padding:0;border:1px solid transparent;border-radius:4px;
  background:transparent;color:#101115;font-size:13px;cursor:pointer}
.rt-btn:hover{background:#E4E7EE;border-color:#C9CCD6}
/* PRESSED, AND IT LOOKS PRESSED. Owner 2026-09-09: "When a style like bold or
   italic is selected, it should have the selected picture. Like a box around
   it." Driven by queryCommandState in app.js, so it follows the CARET -- click
   into bold text and B lights up, which is what eBay's own toolbar does. */
.rt-btn.on{background:#DBE4F5;border-color:#9DB4E0;color:#1D3F8A}
.rt-btn.on:hover{background:#CFDBF2}
.rt-btn svg{width:15px;height:15px}
.rt-colour{width:28px;height:28px;padding:0;border:1px solid #C9CCD6;
  border-radius:4px;background:#fff;cursor:pointer}
.rt-code-lbl{display:inline-flex;align-items:center;gap:5px;margin-left:auto;
  color:#3F4152;font-size:12px;white-space:nowrap;cursor:pointer}
.rt-code-lbl input{width:14px;height:14px;accent-color:var(--accent)}
/* The editable surface. Its type is eBay's, not ours, for the same reason the
   ground is white. */
.rt-body{min-height:210px;padding:12px 14px;overflow:auto;
  font:15px/1.6 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,
  Arial,sans-serif;color:#101115;background:#fff}
.rt-body:focus{outline:none}
.rt-body p{margin:0 0 12px}
.rt-body img{max-width:100%;height:auto}
.rt-body table{max-width:100%}
/* THE SOURCE IS HIDDEN, NOT REMOVED -- it is still the value every reader of
   #description and #tmpl reads. `Show HTML Code` is the only thing that puts it
   on screen. */
.rt .rt-source{display:none}
/* CAPPED ON THE LISTING PAGE ONLY. Owner 2026-09-10: "limit the description
   height so that users dont have to scroll down the page to see their price
   etc. The expanded view is the point of sight of the whole description."

   A long template grew the card until the condition and price were below the
   fold. The box now scrolls inside itself past this height and Expand is how
   the whole thing is read. Preferences is not capped: autoGrow sizes the
   template box there, and a settings page has nothing under it to protect. */
.nl-details .rt-body,
.nl-details .rt.rt-code-on .rt-source{max-height:min(320px,38vh);overflow:auto}
/* ONE SURFACE AT A TIME. Owner 2026-09-09: "When clicking 'show HTML', the
   description itself should switch to HTML, not a text under the current."
   Both were on screen, so the seller had the rendered description and its
   source stacked and had to work out which one they were editing. */
.rt.rt-code-on .rt-body{display:none}
.rt.rt-code-on .rt-source{display:block;width:100%;min-height:210px;border:0;
  border-radius:0;background:#FBFBFD;color:#101115;
  font:13px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  padding:12px 14px;resize:vertical}
/* THE EDITOR INSIDE PREFERENCES, and this needs an id to be heard at all.
   Owner 2026-09-09: "Within the tool bar of the description, the font and size
   are not showing up within preferences."

   `#prefs-body input[type=text], #prefs-body textarea, #prefs-body select`
   sets padding:10px 14px and width:100% for that whole screen. An id beats any
   number of classes, so it beat .rt-sel -- and 10px of vertical padding inside
   a 28px border-box select leaves a SIX PIXEL content box, which clipped
   "Arial" and "14" down to the slivers he saw as dots. The controls were never
   missing; they were squashed.

   Matched here at id level rather than widened with !important, so the
   toolbar's own rules stay the readable ones. */
#prefs-body .rt-sel{padding:0 6px;font-size:12px;width:auto;max-width:132px;
  border-radius:4px;background:#fff;color:#101115;border:1px solid #C9CCD6}
#prefs-body .rt-sel-sm{max-width:64px}
#prefs-body .rt-source{border:0;border-radius:0;background:#FBFBFD;color:#101115;
  font:13px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  padding:12px 14px}
#prefs-body .rt-btn,#prefs-body .rt-colour{width:28px;height:28px;padding:0;
  border-radius:4px}
#prefs-body .rt-btn{border:1px solid transparent;background:transparent}
#prefs-body .rt-btn.on{background:#DBE4F5;border-color:#9DB4E0;color:#1D3F8A}
@media (prefers-reduced-motion:reduce){.rt-btn{transition:none}}
/* The expanded preview fills the dialog the way the expanded editor does, so
   the two faces of the description box open to the same size. .desc-preview
   carries a min-height for the inline case; here the panel decides. */
.tmpl-panel .desc-big-frame{flex:1 1 auto;min-height:0;height:auto;width:100%;
  border:1px solid var(--line);border-radius:var(--r);background:#fff}

/* THE RAIL STARTS WHERE THE FIRST CARD STARTS, AND RUNS PAST IT.

   Owner 2026-09-08: "The right 3 boxes should be aligned with the two
   horizontal left boxes. It should hang over."

   The shared shell aligns both columns at the top, which is right -- but the
   rail's cards were spaced on their own gap while the main column's cards
   carry their own margins, so the two sets of tops drifted apart down the
   page. Same gap on both, measured from the same edge: the first card in each
   column starts on the same line, and the rail is free to run longer than the
   column beside it. */
#view-billing .page-main{display:grid;gap:16px;align-content:start}
#view-billing .page-main > *{margin:0}

/* ---- the plan dialog ---- */
.bm-pm-panel{max-width:940px;width:min(94vw,940px)}
.bm-pm-title{margin:0;font-size:19px;font-weight:700;letter-spacing:-.01em}
.bm-pm-sub{margin:6px 0 0;font-size:13px}
/* WHAT YOU ARE ABOUT TO AGREE TO. It appears only once a plan is armed, which
   is why it can afford to be a surface of its own: it is never noise. */
.bm-pm-review{margin-top:18px;padding:14px;border-radius:var(--r);
  background:var(--bg);border:1px solid var(--line)}
.bm-pm-sum{margin:0;font-size:14px;font-weight:600}
.bm-pm-effect{margin:6px 0 0;font-size:13px;line-height:1.5;color:var(--ink-soft)}
/* Confirm on the right, the way every dialog in this app and every dialog the
   seller uses elsewhere puts it. */
.bm-pm-acts{display:flex;justify-content:flex-end;gap:10px;margin-top:18px}
@media (max-width:560px){
  .bm-pm-acts{flex-direction:column-reverse}
  .bm-pm-acts button{width:100%}
}

.bm-msg{margin:14px 0 0;font-size:14px;line-height:1.5;color:var(--ink-soft)}
.bm-msg.err{color:var(--danger-ink)}
.bm-msg.warn{color:var(--med,#8A5312)}

/* ============ show password ============
   The reset page drew its own since it shipped; login, signup and the app's
   password prompts had none until 2026-09-05. Edge draws a native reveal that
   VANISHES the moment script sets the value, leaving its own control looking
   broken, and Firefox and Safari draw none at all -- so the native one is
   suppressed and this is the reveal everywhere. */
input[type=password]::-ms-reveal,
input[type=password]::-ms-clear{display:none}
.pw-wrap{position:relative}
.pw-wrap input{padding-right:44px}
.pw-eye{position:absolute;top:50%;right:10px;transform:translateY(-50%);
  width:28px;height:28px;display:grid;place-items:center;color:var(--muted);
  background:none;border:0;padding:0;cursor:pointer}
.pw-eye svg{width:20px;height:20px}
.pw-eye:hover{color:var(--ink-soft)}
.pw-eye:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:4px}

/* The plan picker. One row per tier on a wide screen, stacked below; each is a
   button because choosing one is an action, and the chosen one is pressed
   rather than merely coloured so it reads without relying on colour. */
/* THE PLAN PICKER: six vertical choice blocks (owner 2026-09-06, "Its just a
   vertical choice blocks and should be presented nicely").

   A LIST, NOT A GRID. It was auto-fit columns, which reflowed into two or
   three depending on width and made the reader hunt across cards for the one
   number that changed. Six plans differing in three facts are read by running
   DOWN a column, so each row is full width and the three facts hold fixed
   positions: price at the leading edge, allowance beside it, rate below.

   Space groups them, not lines -- 10px inside a row against 24px above the
   group, comfortably the 2x the grouping rule asks for. The tick, the border
   and the weight all carry the selected state, so it never rests on colour
   alone. */
/* HORIZONTAL, and auto-fit rather than a fixed count. Owner 2026-09-08:
   "why is everything vertically placed" -- the five plans were a column of
   full-width rows, so comparing them meant scrolling past four to see the
   fifth. auto-fit at 190px puts all five in a row on a wide screen and wraps
   to two rows on a laptop without a second breakpoint to keep in step. */
.bm-plans{display:grid;gap:10px;margin:18px 0 4px;
  grid-template-columns:repeat(auto-fit,minmax(190px,1fr))}
.bm-plan{display:flex;flex-direction:column;align-items:flex-start;gap:3px;
  width:100%;position:relative;
  padding:14px 16px;border:1px solid var(--line);border-radius:var(--r);
  background:var(--card);color:inherit;font:inherit;cursor:pointer;
  text-align:start;
  /* Named, never `all`: a transition on every property animates the layout
     when the tick appears. */
  transition:border-color 150ms ease-out, box-shadow 150ms ease-out}
.bm-plan:hover{border-color:var(--accent)}
.bm-plan:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
/* Inset rather than a second border: an outer ring would change the row's
   height on selection and nudge every row below it. */
/* TWO STATES NOW, AND THEY ARE DIFFERENT THINGS. `.on` is the plan you are
   already on -- a fact, drawn quietly. `.armed` is the one you have picked in
   this dialog and not yet confirmed, which is the only one that gets the
   accent. Drawing them the same is how a seller reads "this is your plan" as
   "this is about to happen". */
.bm-plan.on{border-color:var(--line-strong)}
.bm-plan.armed{border-color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent)}
/* The static cue the selected state needs beside colour. Concentric with the
   row: 999px is a circle, which is what a radio is, and the only place on the
   site a full round is correct. */
.bm-plan-tick{position:absolute;top:12px;inset-inline-end:12px;
  width:16px;height:16px;
  border:1px solid var(--field);border-radius:999px;flex:none;
  transition:border-color 150ms ease-out, box-shadow 150ms ease-out}
.bm-plan.armed .bm-plan-tick{border-color:var(--accent);
  box-shadow:inset 0 0 0 4px var(--accent)}
.bm-plan-now{margin-top:6px;font-size:11px;font-weight:700;letter-spacing:.04em;
  text-transform:uppercase;color:var(--muted)}
.bm-plan-price{font-size:19px;font-weight:700;font-variant-numeric:tabular-nums;
  white-space:nowrap}
.bm-plan-price small{font-size:12px;font-weight:600;color:var(--muted);
  margin-inline-start:2px}
.bm-plan-incl{font-size:14px;font-weight:600}
.bm-plan-rate{font-size:13px;color:var(--muted)}

/* The trial offer. Its own surface inside the card, because it is a different
   kind of thing from the list above it: one action, once, with a deadline. */
.bm-trial{display:flex;flex-wrap:wrap;gap:12px 20px;align-items:center;
  justify-content:space-between;margin-top:24px;padding:16px 18px;
  border-radius:var(--r);background:var(--tint)}
.bm-trial-say{flex:1 1 16rem;min-width:0}
.bm-trial-h{margin:0;font-size:14px;font-weight:700}
.bm-trial-p{margin:4px 0 0;font-size:13px;line-height:1.5;color:var(--ink-soft)}

/* ---- the app on a phone ----------------------------------------------------

   Owner 2026-09-05: "Make sure the website is supported for phone view."

   THE RAIL IS THE WHOLE PROBLEM. .shell is a 236px column plus the content,
   and 236px of a 390px phone is most of it -- the app rendered into the 154px
   that were left, which is not a narrow layout, it is an unusable one. Nothing
   below 1050px addressed it: those breakpoints fold the two-column PAGES, and
   the shell holding them stayed at two.

   SO THE RAIL STOPS BEING A RAIL. Below 760px it becomes a horizontal strip
   above the content -- the same tabs, the same order, the same active state,
   scrolled sideways when they do not fit. A drawer behind a hamburger would
   hide the app's only navigation behind a control that has to be discovered;
   the tabs are worth their line.

   AND THE COLLAPSE MACHINERY IS NEUTRALISED HERE, deliberately and by name.
   `side-collapsed` and `side-peek` are added by JS from the button and from
   pointer-enter, and both drive grid-template-columns. Left alone they would
   fight this layout from a class the seller cannot see or remove on a device
   with no pointer to hover. The button itself is hidden, so the classes cannot
   be reached from here -- but a stale class on a resized window can, and that
   is the case these three lines are for. */
@media (max-width:760px){
  .shell,
  .shell.side-collapsed,
  .shell.side-collapsed.side-peek{grid-template-columns:1fr}
  .shell > .main{grid-column:1}

  .side,
  .shell.side-collapsed .side,
  .shell.side-collapsed.side-peek .side{position:sticky;top:0;z-index:20;
    height:auto;flex-direction:row;align-items:center;gap:10px;
    padding:8px 12px;border-right:0;border-bottom:1px solid var(--line);
    overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch}
  /* The mark keeps its place at the head of the strip and stops shrinking --
     a flex row will squash an image before it scrolls. */
  .side-logo{flex:none}
  .side-logo img{height:38px;width:auto;margin:0 6px 0 0}
  /* The collapse control is meaningless once the rail is a strip: there is no
     second column to give back. */
  .side-collapse{display:none}
  .side-nav{flex-direction:row;gap:4px;overflow:visible}
  .side-nav .tab{flex:none;white-space:nowrap;padding:7px 11px;
    min-height:38px}
  /* The connection card is pinned to the bottom of the rail with margin-top:auto,
     which in a row pushes it off the end. It has its own place on the dashboard
     and does not need to ride the navigation. */
  .side .conn-card{display:none}

  .main{padding:18px 14px 32px}
  .page-head{flex-wrap:wrap;gap:10px}
  /* Toolbars that sat beside a heading go under it rather than squeezing it to
     a couple of characters. */
  .page-head .drafts-tools{margin-left:0;width:100%}
  .drafts-tools > button{flex:1 1 auto}

  /* The description editor's toolbar already wraps; its controls just need to
     stay big enough to hit. 40px is the usual floor for a touch target. */
  .rt-btn,.rt-colour{width:34px;height:34px}
  .rt-bar{gap:5px}
  /* The checkbox drops to its own line rather than being pushed off the end by
     the controls it follows. */
  .rt-code-lbl{margin-left:0;width:100%}

  /* Anything that measures itself against a desktop viewport. */
  .stat-strip{gap:10px}
  .modal .panel{width:auto;max-width:none;margin:0 8px}
}

/* ===== ui-ux pass 2026-09-10 (public pages) =====
   Measured over the DevTools protocol with real phone emulation at 375, 768,
   1024 and 1440 (a plain --window-size screenshot is a known false signal on
   this site, see docs/reviews/2026-08-24-homepage-taste-review.md). Contrast
   passed everywhere; what failed was target size and touch density.

   WCAG 2.2 AA 2.5.8 asks 24 CSS px of every pointer target that is not inline
   in a sentence. The nav "Log in" measured 37x21 on every public page and the
   logo link 130x23 on a phone. Touch guidance (44pt / 48dp) applies only to
   the phone breakpoint, where these are the only controls above the fold. */
.mk-nav>a:first-child{display:inline-flex;align-items:center;min-height:24px}
.mk-login{display:inline-flex;align-items:center;min-height:24px;padding:0 4px;margin:0 -4px}
/* The current page is marked in the markup (aria-current), not only by a
   class, so a screen reader hears it and the colour is a consequence. */
.mk-links a[aria-current="page"]{color:var(--ink)}
/* .cta-ghost was used on the pricing page and defined nowhere, so it rendered
   as a bare 24px link beside a 46px button. A quiet secondary button: same
   height and radius as .btn-pill, hairline instead of fill. */
.cta-ghost{display:inline-flex;align-items:center;justify-content:center;min-height:46px;
  padding:0 20px;border:1px solid var(--line-strong);border-radius:4px;color:var(--ink);
  font-weight:600;font-size:16px;text-decoration:none;
  transition:border-color .15s,background-color .15s}
.cta-ghost:hover{border-color:var(--ink);background:var(--bg)}
/* No 300ms tap delay, no accidental double-tap zoom on a button. */
body.mk a,body.mk button,body.mk summary,body.auth a,body.auth button{touch-action:manipulation}
/* 100vh on a phone is the height BEHIND the browser chrome; dvh is the height
   the reader sees. Guarded so older engines keep the vh they already had. */
@supports (min-height:100dvh){body.mk{min-height:100dvh}}
/* Balanced wrapping on the short display headings. A hint to the engine,
   not a promise about which word lands on the last line. */
.mk-hero h1,.mk-faq h2,.learn-copy h2,.mk-c-left h1,.nf h1,.mk-band h2{text-wrap:balance}
@media (max-width:640px){
  /* the nav row and the footer chips are the only controls a thumb meets on
     every page; 44px tall on touch, unchanged on desktop */
  .mk-links a{padding:10px 0}
  .mk-login{min-height:44px;padding:0 8px;margin:0 -8px}
  /* body.mk in front: home.html and contact.html restate this padding
     in their own phone blocks, which load after this file */
  body.mk .mk-footer nav a{padding:12px 14px}
  body.mk .mk-footer .mk-legal a{min-height:44px;padding:0 4px}
}
