/* App CSS Migration (shared components only) -- started 2026-07-21, per John.
   Spec: docs/plans/APP_CSS_MIGRATION_SPEC.md.
   Hand-authored, not build-generated -- served directly by the Cloudflare
   Workers [assets] binding, same mechanism as public/app.js. Linked from
   client.ts's INDEX_HTML via a plain <link rel="stylesheet"> tag.
   Uses the :root custom properties already defined inline in INDEX_HTML
   (the 2026-07-17 CSS variables migration) -- this file does NOT redefine
   tokens, only consumes var(--x).
   Batch 1 (this pass): Lbl, SectionLabel, Badge, IconBtn, EmptyState (new).
   Remaining components migrate in later passes per the spec's sequencing. */

.wb-lbl{
  font-size:11px;
  font-weight:600;
  color:var(--muted);
  text-transform:uppercase;
  letter-spacing:.05em;
  margin-bottom:4px;
}

.wb-section-label{
  font-size:10px;
  font-weight:700;
  color:var(--accent);
  text-transform:uppercase;
  letter-spacing:.12em;
  margin-bottom:6px;
  opacity:.8;
}

/* Badge's color is per-instance (severity/category color, not a fixed
   token) -- kept as a CSS custom property set inline per call site
   (--badge-color), same pattern as this file already relies on for
   var(--accent) etc. This keeps the base shape (padding/radius/font/
   border-derivation) in one place while the actual color stays dynamic. */
.wb-badge{
  background:color-mix(in srgb,var(--badge-color) 13%,transparent);
  color:var(--badge-color);
  border:1px solid color-mix(in srgb,var(--badge-color) 27%,transparent);
  border-radius:8px;
  padding:2px 8px;
  font-size:11px;
  font-weight:600;
  white-space:nowrap;
}
.wb-badge--wrap{white-space:normal;}
.wb-badge--clickable{cursor:pointer;font-family:inherit;}

.wb-icon-btn{
  display:flex;
  align-items:center;
  justify-content:center;
  gap:4px;
  background:transparent;
  border:none;
  border-radius:6px;
  color:var(--muted);
  cursor:pointer;
  padding:4px 6px;
  font-size:12px;
  line-height:1;
  flex-shrink:0;
  transition:all .15s;
}
.wb-icon-btn:disabled{color:var(--border);cursor:default;}
.wb-icon-btn--active{background:var(--accent-muted);color:var(--accent);}
.wb-icon-btn--bordered{border:1px solid var(--border);}
.wb-icon-btn--bordered.wb-icon-btn--active{border-color:var(--accent);}

/* New component, extracted this pass -- was copy-pasted identically in
   completed.js and projplans.js (both gained a search box 2026-07-17,
   same session, same inline block hand-duplicated instead of shared).
   See docs/plans/APP_CSS_MIGRATION_SPEC.md's Vetting pass section. */
.wb-empty-state{
  text-align:center;
  padding:24px 20px;
  color:var(--muted);
  font-size:13px;
}

/* ============================================================
   Batch 2 (2026-07-21): Toggle, Checkbox, Radio, DateField, Tab, NavRow.
   Established pattern for components with genuinely per-instance colors
   (Toggle's w/h, Tab's activeBg/activeColor/inactiveColor): the STRUCTURAL
   shape (layout, transitions, borders, radius) lives in the class; the
   truly dynamic values ride as CSS custom properties set inline on the
   element (e.g. --tab-color), referenced via var(--x, fallback) so the
   class still has a sane default when no override is passed. Same idea
   Badge/IconBtn already established in batch 1, just formalized here.
   ============================================================ */

.wb-toggle{
  display:inline-flex;
  align-items:center;
  justify-content:flex-start;
  border:none;
  padding:2px;
  box-sizing:border-box;
  flex-shrink:0;
  cursor:pointer;
  background:var(--border);
  width:var(--toggle-w);
  height:var(--toggle-h);
  border-radius:var(--toggle-h);
  transition:background .18s cubic-bezier(0.4,0,0.2,1);
}
.wb-toggle--on{background:var(--accent);justify-content:flex-end;}
.wb-toggle:disabled{cursor:not-allowed;opacity:.5;}
.wb-toggle-knob{
  display:block;
  border-radius:50%;
  background:#fff;
  box-shadow:0 1px 2px rgba(0,0,0,.3);
  transition:transform .18s cubic-bezier(0.4,0,0.2,1);
  width:calc(var(--toggle-h) - 4px);
  height:calc(var(--toggle-h) - 4px);
}

/* Checkbox/Radio: the hidden-native-input approach (2026-07-18 decision,
   see CUSTOM_FORM_CONTROLS_SPEC.md) previously needed JS-tracked focus
   state (onFocus/onBlur -> useState) specifically because inline styles
   can't react to a sibling/descendant's focus. Real CSS can, via
   :focus-within on the wrapping box -- so this migration also DROPS the
   useState/onFocus/onBlur plumbing from both components, not just moves
   their styling. Same visible behavior (ring appears exactly when the
   real input has focus), fewer moving parts, no longer a documented
   workaround for a limitation that no longer applies once real CSS exists. */
.wb-checkbox-box,.wb-radio-dot{
  position:relative;
  display:inline-flex;
  align-items:center;
  justify-content:center;
  width:var(--ctrl-size);
  height:var(--ctrl-size);
  flex-shrink:0;
  box-sizing:border-box;
  background:var(--bg);
  border:1px solid var(--border);
  transition:background .18s cubic-bezier(0.4,0,0.2,1),border-color .18s cubic-bezier(0.4,0,0.2,1),box-shadow .18s cubic-bezier(0.4,0,0.2,1);
}
.wb-checkbox-box{border-radius:4px;}
.wb-radio-dot{border-radius:50%;}
.wb-checkbox-box--checked{background:var(--accent);border-color:var(--accent);}
.wb-radio-dot--checked{border-color:var(--accent);}
.wb-checkbox-box:focus-within,.wb-radio-dot:focus-within{box-shadow:0 0 0 2px color-mix(in srgb,var(--accent) 33%,transparent);}
.wb-checkbox-box--disabled,.wb-radio-dot--disabled{opacity:.5;}
.wb-radio-inner{border-radius:50%;background:var(--accent);transition:transform .18s cubic-bezier(0.4,0,0.2,1);}
.wb-ctrl-input{position:absolute;top:0;left:0;width:100%;height:100%;margin:0;opacity:0;cursor:pointer;}
.wb-ctrl-input:disabled{cursor:not-allowed;}
.wb-form-label{display:inline-flex;align-items:center;gap:8px;cursor:pointer;}
.wb-form-label--disabled{cursor:not-allowed;opacity:.5;}
.wb-form-label-text{font-size:13px;color:var(--text);}

.wb-date-field{
  background:var(--bg);
  border:1px solid var(--border);
  border-radius:10px;
  color:var(--text);
  padding:7px 10px;
  font-size:13px;
  width:100%;
  box-sizing:border-box;
  outline:none;
}
.wb-date-field:disabled{opacity:.5;}

.wb-nav-row{
  display:flex;
  flex-direction:row;
  align-items:center;
  justify-content:flex-start;
  gap:8px;
  width:100%;
  padding:9px 10px;
  border-radius:8px;
  border:none;
  background:transparent;
  color:var(--muted);
  cursor:pointer;
  font-size:12px;
  font-weight:400;
  margin-bottom:0;
}
.wb-nav-row--active{background:var(--accent-muted);color:var(--accent);font-weight:600;}
.wb-nav-row--column{flex-direction:column;justify-content:center;gap:2px;width:auto;flex:1 0 auto;min-width:60px;position:relative;}

.wb-tab{
  padding:7px 12px;
  border-radius:8px;
  border:none;
  box-sizing:border-box;
  cursor:pointer;
  font-size:12px;
  font-weight:400;
  background:transparent;
  color:var(--tab-inactive-color,var(--muted));
  transition:all .15s;
}
.wb-tab--active{
  font-weight:600;
  background:linear-gradient(180deg,color-mix(in srgb,var(--tab-active-bg,var(--accent)) 100%,#fff 8%),var(--tab-active-bg,var(--accent)) 70%);
  color:var(--tab-active-color,#1a0e00);
  box-shadow:0 2px 8px color-mix(in srgb,var(--tab-active-bg,var(--accent)) 22%,transparent);
}
.wb-tab--tinted{border:1px solid var(--border);}
.wb-tab--tinted.wb-tab--active{
  border-color:var(--tab-active-bg,var(--accent));
  background:color-mix(in srgb,var(--tab-active-bg,var(--accent)) 13%,transparent);
  color:var(--tab-active-bg,var(--accent));
  font-weight:700;
}
.wb-tab--left{text-align:left;}
.wb-tab--block{display:block;width:100%;}
.wb-tab--disabled{opacity:.5;cursor:not-allowed;}
/* Line/underline variant (2026-07-24, TAB_ENHANCEMENTS_SPEC.md Phase 1) -- lighter-weight
   style for in-modal sub-navigation, distinct from the solid-fill/tinted-pill styles above
   used for top-level section switching. Mobile-first: this rule is already correct at any
   width, no desktop-only override needed (unlike layout-affecting components, an underline
   tab's size doesn't change by viewport). */
.wb-tab--line{
  border-radius:0;
  border-bottom:2px solid transparent;
  padding:8px 4px;
  margin-right:16px;
}
.wb-tab--line.wb-tab--active{
  background:transparent;
  color:var(--tab-active-bg,var(--accent));
  border-bottom-color:var(--tab-active-bg,var(--accent));
}

/* TabBar sliding indicator (2026-07-24, TAB_ENHANCEMENTS_SPEC.md Phase 2). Built and
   styled for the line/underline variant -- TabBar's only real caller so far
   (TileDetailModal, Phase 3). A solid/tinted-background indicator would need
   different positioning (filled rect behind the tab, not a bottom bar) -- not
   built now since there's no real caller for that combination yet; extend this
   rule rather than guessing its shape if one shows up later. */
.wb-tab--line{position:relative;z-index:1;}
.wb-tabbar-indicator{
  position:absolute;
  bottom:0;
  left:0;
  height:2px;
  background:var(--accent);
  transition:transform .22s cubic-bezier(0.4,0,0.2,1),width .22s cubic-bezier(0.4,0,0.2,1);
}
@media (prefers-reduced-motion: reduce){
  .wb-tabbar-indicator{transition:none;}
}

/* ============================================================
   Batch 3 (2026-07-21): Inp, Sel, Txt, Card. All 4 already merged p.style
   on top of their defaults (Object.assign(defaults, p.style)) -- inline
   style always wins over a class regardless, so per-instance overrides
   keep working exactly as before with zero changes needed at call sites.
   ============================================================ */

.wb-input,.wb-select,.wb-textarea{
  background:var(--bg);
  border:1px solid var(--border);
  border-radius:10px;
  color:var(--text);
  padding:8px 12px;
  font-size:13px;
  outline:none;
  width:100%;
  box-sizing:border-box;
}
.wb-textarea{padding:10px;resize:vertical;font-family:inherit;}
.wb-textarea--mono{font-family:monospace;}

/* Sel's chevron color (#c8aa64) and forced color-scheme:dark are both
   pre-existing behavior, carried over unchanged -- neither followed the
   live accent/theme before this migration either (a data-URI background-
   image can't reference a CSS custom property, and color-scheme was never
   conditional on the app's Light/Dark setting). Not a regression introduced
   here, and not silently "fixed" either -- flagging as a possible future
   look, same as the spec's own scope (style relocation, not behavior
   changes) says it should. */
.wb-select{
  padding-right:28px;
  -webkit-appearance:none;
  -moz-appearance:none;
  appearance:none;
  background-image:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"><path d="M3 5l4 4 4-4" stroke="%23c8aa64" stroke-width="1.6" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>');
  background-repeat:no-repeat;
  background-position:right 10px center;
  color-scheme:dark;
}

.wb-card{
  background:var(--card);
  border:1px solid var(--card-border,var(--border));
  border-radius:14px;
  padding:14px;
  cursor:default;
  box-shadow:0 1px 0 rgba(255,255,255,.03) inset,0 2px 8px rgba(0,0,0,.35);
}
.wb-card--clickable{cursor:pointer;}

/* ============================================================
   Batch 4 (2026-07-22): Btn, TileDetailModal -- the two highest-usage/
   highest-risk components per the spec's own sequencing.
   ============================================================ */

/* Real behavior note, not just a style move: the OLD inline-JS version
   merged p.style onto `base` FIRST, then merged the variant object on TOP
   of that (Object.assign({},base,v[p.variant])) -- meaning a variant's
   own background/color/padding always WON over anything the caller passed
   in p.style for those same properties, silently. Real CSS doesn't work
   that way -- an inline style attribute always beats a class, full stop --
   so this migration flips that precedence: p.style now correctly overrides
   the variant. Only one existing call site was actually affected by this
   (AdminCopyBlock's "Copied" background highlight, src/core/p3.js) -- its
   copied-state highlight was silently never rendering because of the old
   quirk; it renders correctly now. Admin-only surface, low risk, and
   matches what that code was clearly always trying to do. */
.wb-btn{
  display:inline-flex;
  align-items:center;
  gap:6px;
  padding:9px 18px;
  border-radius:999px;
  border:none;
  cursor:pointer;
  font-size:13px;
  font-weight:600;
  transition:all .18s cubic-bezier(0.4,0,0.2,1);
  box-sizing:border-box;
}
.wb-btn:disabled{cursor:not-allowed;opacity:.5;}
.wb-btn--sm{padding:5px 14px;font-size:11px;}
.wb-btn--full{width:100%;justify-content:center;}
.wb-btn--primary{background:linear-gradient(180deg,color-mix(in srgb,var(--accent) 100%,#fff 8%),var(--accent) 65%);color:#1a0e00;box-shadow:0 3px 12px color-mix(in srgb,var(--accent) 30%,transparent);}
.wb-btn--secondary{background:var(--border);color:var(--text);}
.wb-btn--ghost{background:transparent;color:var(--muted);padding:6px 10px;}
.wb-btn--ghost.wb-btn--sm{padding:3px 7px;}
.wb-btn--danger{background:#4a1515;color:#f87171;}
.wb-btn--print{background:#3b1f6e;color:var(--print);border:1px solid color-mix(in srgb,var(--print) 27%,transparent);}

/* TileDetailModal: the shared project-detail overlay -- also the component
   at the center of the earlier modal z-index saga this session (zIndex
   9999, see Pro-Tip #33). Structural shape only moves to CSS here; the
   zIndex value itself stays exactly 9999, unchanged, matching every other
   modal in the app that was tuned relative to it. */
.wb-modal-backdrop{
  position:fixed;
  inset:0;
  background:rgba(0,0,0,0.75);
  z-index:9999;
  display:flex;
  align-items:center;
  justify-content:center;
  padding:16px;
}
.wb-modal-card{
  background:var(--card);
  border:1px solid var(--border);
  border-radius:14px;
  padding:18px;
  width:100%;
  max-height:88vh;
  overflow-y:auto;
}

/* LightboxHost: fully static, no variants -- last piece of the corrected
   20-component list from the spec's vetting pass. zIndex 10000, unchanged
   (already the highest in the app, deliberately above TileDetailModal's
   9999). */
.wb-lightbox-backdrop{
  position:fixed;
  inset:0;
  background:rgba(0,0,0,0.88);
  z-index:10000;
  display:flex;
  align-items:center;
  justify-content:center;
  padding:24px;
  cursor:zoom-out;
}
.wb-lightbox-frame{
  width:92vw;
  height:92vh;
  display:flex;
  align-items:center;
  justify-content:center;
}
.wb-lightbox-img{
  width:100%;
  height:100%;
  object-fit:contain;
  border-radius:8px;
  box-shadow:0 8px 40px rgba(0,0,0,.6);
}

/* BuildingTabBar: trivial flex wrapper around Tab, static, no variants. */
.wb-tabbar{
  display:flex;
  gap:6px;
  margin-bottom:14px;
  flex-wrap:wrap;
}

/* TypeTilePicker: selectable grid tiles. --tile-cols is per-instance (grid
   column count varies by caller). --tile-color/--tile-tint are the
   per-instance accent + tint passed by the caller (defaults to the live
   theme accent/accent-muted) -- same "structural shape in the class,
   dynamic color as a custom property" pattern as Badge/Tab. Selected state
   is a boolean, not a continuous value, so it stays a plain modifier class
   like Toggle/Checkbox rather than another custom property. */
.wb-tile-grid{
  display:grid;
  grid-template-columns:repeat(var(--tile-cols,5),1fr);
  gap:6px;
  margin-bottom:var(--tile-mb,12px);
}
.wb-tile{
  background:var(--bg);
  border:1px solid var(--border);
  border-radius:8px;
  padding:6px 3px;
  cursor:pointer;
  display:flex;
  flex-direction:column;
  align-items:center;
  gap:3px;
  transition:all .15s;
}
.wb-tile--sel{
  background:var(--tile-tint,var(--accent-muted));
  border-color:var(--tile-color,var(--accent));
}
.wb-tile-label{
  font-size:10px;
  font-weight:400;
  color:var(--text);
  text-align:center;
}
.wb-tile-label--sel{
  font-weight:700;
  color:var(--tile-color,var(--accent));
}

/* ============================================================
   Batch 6 (2026-07-22): raw card-shaped divs -- ~9 call sites across 9
   files that hand-rolled Card's look instead of calling the shared
   component. Modifiers layered onto .wb-card (batch 3), same hybrid
   pattern as Badge/Tab: structural shape here, per-instance color as a
   CSS custom property. Full inventory + risk tiers:
   docs/plans/APP_CSS_MIGRATION_BATCH6_SPEC.md
   ============================================================ */

/* Community feed post card's dynamic accent stripe (difficulty color). */
.wb-card--top-accent{
  border-top:3px solid var(--card-top-accent,var(--accent));
}

/* Compact card variant: grid tiles (Inventory grid view, 3DWright/QR
   cards) -- tighter radius/padding than the default Card. */
.wb-card--compact{
  border-radius:8px;
  padding:10px;
}

/* Row variant: Card's base is a block; some sites (inventory value bar,
   Shop list rows) only ever needed the surface/border, laid out as a
   flex row, not Card's own block padding/shadow defaults. */
.wb-card--row{
  display:flex;
  align-items:center;
  gap:10px;
  padding:8px 14px;
}
.wb-card--row.wb-card{
  box-shadow:none;
}

/* ============================================================
   Batch 7 (2026-07-22): repeated inline patterns beyond Card.
   Both fully static -- no per-instance dynamic values, unlike batches
   1-6's Badge/Tab/TypeTilePicker/etc hybrid pattern. Full inventory +
   risk profile: docs/plans/APP_CSS_MIGRATION_BATCH7_SPEC.md
   ============================================================ */

/* Tool Hub calculator explainer text -- identical across all 7
   calculator tabs (Fit, Angle/Miter x2, Board Feet, Sheet Yield, Scale,
   Wood Movement). */
.wb-calc-hint{
  font-size:11px;
  color:var(--muted);
  margin-top:10px;
  line-height:1.5;
}

/* Tool Hub calculator input row -- identical wrapper across all 6
   calculator tabs' input fields. */
.wb-calc-row{
  display:flex;
  gap:10px;
  align-items:flex-end;
  flex-wrap:wrap;
  margin-bottom:14px;
}

/* Card grid layout -- responsive auto-fill grid, identical across 3DWright
   models, Community feed (incl. its loading-skeleton state), and the
   generic "Printable results" grid. Pure layout, no variants. */
.wb-card-grid{
  display:grid;
  grid-template-columns:repeat(auto-fill,minmax(220px,1fr));
  gap:12px;
}

/* Compact variant -- Inventory's grid-view toggle (small icon tiles, not
   content cards). Same auto-fill pattern, smaller minimum + tighter gap +
   a top margin the base class doesn't need (sits directly under a search
   bar). batch 8, 2026-07-22, per John: pure CSS-hygiene, zero visual change. */
.wb-card-grid--sm{
  display:grid;
  grid-template-columns:repeat(auto-fill,minmax(108px,1fr));
  gap:8px;
  margin-top:4px;
}

/* PreloaderRing (BRAND_GUIDELINES.md 9.3-D) -- indeterminate rotation, no
   fake progress fill (no real progress signal exists for either app boot
   or a button-level fetch). 2026-07-22. */
.wb-ring-spin{
  animation:wbRingSpin 1s linear infinite;
}
@keyframes wbRingSpin{
  to{transform:rotate(360deg);}
}
@media (prefers-reduced-motion: reduce){
  .wb-ring-spin{animation:none;}
}

/* == Toast enter/exit animation (2026-07-24, FIELD_FORM_GROUPS_SPEC.md folded-in fix) ==
   .wb-toast--enter/--exit added by src/core/p4.js's toast render; the two-phase
   exiting:true -> filter timing lives in src/core/p1.js's addToast/removeToastNow
   (WB_TOAST_EXIT_MS=260, must match the exit animation duration below). Stack itself
   gets a transition on reflow so surviving toasts slide smoothly when one is removed. */
.wb-toast-stack{transition:none;}
.wb-toast{
  transition:transform .22s cubic-bezier(0.4,0,0.2,1);
}
.wb-toast--enter{
  animation:wbToastIn .32s cubic-bezier(0.4,0,0.2,1);
}
.wb-toast--exit{
  animation:wbToastOut .26s cubic-bezier(0.4,0,0.2,1) forwards;
}
@keyframes wbToastIn{
  from{opacity:0;transform:translateY(14px);}
  to{opacity:1;transform:translateY(0);}
}
@keyframes wbToastOut{
  from{opacity:1;transform:translateY(0);}
  to{opacity:0;transform:translateY(8px);}
}
@media (prefers-reduced-motion: reduce){
  .wb-toast--enter,.wb-toast--exit{animation:none;}
  .wb-toast{transition:none;}
}

/* == Eyebrow label utility (2026-07-24, FIELD_FORM_GROUPS_SPEC.md templatized-pattern fix) ==
   App-side equivalent of marketing.ts's `.label` class (Section 9.2) -- same visual
   convention (small uppercase honey-tinted eyebrow text), extracted once here so
   FieldGroup's group-title and any future app-side eyebrow use share one definition
   instead of a third near-identical style appearing later. */
.wb-eyebrow{
  font-size:11px;
  letter-spacing:.14em;
  text-transform:uppercase;
  color:var(--accent);
  font-weight:600;
}

/* == FieldGroup / Field (2026-07-24, FIELD_FORM_GROUPS_SPEC.md) ==
   Wraps existing Inp/Sel/Txt/Checkbox/Radio/DateField -- no new input primitives.
   Mobile-first per Section 9.1: .wb-field-row stacks by default, row layout only
   inside the existing min-width:761px desktop block below. */
.wb-field-group{
  border-left:2px solid color-mix(in srgb,var(--accent) 25%,transparent);
  padding-left:18px;
  margin-bottom:24px;
}
.wb-field-group:last-child{margin-bottom:0;}
.wb-field-group-title{margin-bottom:14px;}
.wb-field{
  display:flex;
  flex-direction:column;
  gap:6px;
  margin-bottom:14px;
}
.wb-field:last-child{margin-bottom:0;}
.wb-field--inline{
  flex-direction:row;
  align-items:center;
  gap:8px;
}
.wb-field-label{
  font-size:12px;
  color:var(--muted);
}
.wb-field-hint{
  font-size:11px;
  color:var(--muted);
}
.wb-field-error{
  font-size:11px;
  color:var(--high);
}
.wb-field-required{
  color:var(--accent);
  margin-left:2px;
}
.wb-field-row{
  display:flex;
  flex-direction:column;
  gap:12px;
}
@media (min-width:761px){
  .wb-field-row{flex-direction:row;}
  .wb-field-row>.wb-field{flex:1;}
}

/* == Skeleton shimmer + skeleton ring (2026-07-24, SKELETON_PRELOADER_BLEND_SPEC.md) ==
   Shimmer reused across every RLSL/section-level loading placeholder instead of
   inline-declared per call site (matches the .wb-icon-btn/.wb-scroll extraction
   discipline already used elsewhere in this file). Ring itself reuses PreloaderRing
   (src/core/p1.js, this file's .wb-ring-spin above) at different sizes -- no
   second ring implementation, per the spec's templatized-pattern finding. */
.wb-skel-shimmer{
  background:linear-gradient(90deg,
    color-mix(in srgb,var(--accent) 8%,transparent) 25%,
    color-mix(in srgb,var(--accent) 18%,transparent) 37%,
    color-mix(in srgb,var(--accent) 8%,transparent) 63%);
  background-size:400% 100%;
  animation:wbSkelShimmer 1.4s ease infinite;
  border-radius:6px;
}
@keyframes wbSkelShimmer{
  0%{background-position:100% 0;}
  100%{background-position:0 0;}
}
.wb-skel-row{
  display:flex;
  gap:14px;
  align-items:center;
  padding:12px 0;
}
.wb-skel-row+.wb-skel-row{
  border-top:1px solid color-mix(in srgb,var(--text) 4%,transparent);
}
.wb-skel-lines{
  flex:1;
  display:flex;
  flex-direction:column;
  gap:8px;
}
.wb-skel-line{
  height:12px;
}
.wb-skel-section{
  display:flex;
  flex-direction:column;
  align-items:center;
  justify-content:center;
  gap:10px;
  padding:36px 0;
}
.wb-skel-section-label{
  font-size:12px;
  color:var(--muted);
  letter-spacing:.05em;
}
@media (prefers-reduced-motion: reduce){
  .wb-skel-shimmer{animation:none;}
}

/* Keyboard focus-visible audit (2026-07-28, per John) -- no global :focus-visible
   rule existed anywhere in this file; mouse users saw hover states, keyboard users
   got the browser default (often invisible against this dark theme) or nothing.
   One consistent rule across all shared interactive components -- accent-colored
   outline + offset so it's visible on both light and dark card backgrounds, doesn't
   rely on any single component's own background color. :focus-visible (not :focus)
   so mouse clicks don't get a ring, only real keyboard nav does. */
.wb-btn:focus-visible,
.wb-icon-btn:focus-visible,
.wb-nav-row:focus-visible,
.wb-input:focus-visible,
.wb-select:focus-visible,
.wb-badge--clickable:focus-visible,
.wb-tab:focus-visible{
  outline:2px solid var(--accent);
  outline-offset:2px;
}
