/* kuribaby-page.css — page chrome owned by kuribaby (everything else is synced CSS). */

/* The faces, the type stack and the colour tokens are in kb-tokens.css, loaded just before this file. */

/* While a shared file is opening, the home page stays invisible under the
   incoming viewer (class set by an inline head script, cleared by the boot). */
html.kb-share-loading #flairMain {
  visibility: hidden;
}

/* ── Night mode (the star-sky black) ──────────────────────────────────────────────
   Classes go on <html> (set pre-paint by a head script, toggled by the switch);
   dark-mode / dark-mode-active are the synced UI's own dark hooks, so panels,
   modals and the viewer follow automatically. These rules cover our page chrome. */
html.dark-mode-active,
html.dark-mode-active body {
  background: var(--color-bg-page);
  color: var(--color-text);
}

/* The night sky (owner, 2026-10-07: night is the star-sky black, with stars). A still field of small
   stars on the page's ground, and a layer of larger ones that slowly brighten and fade. Both are drawn
   in the library's star greys and its light teal and blue; with less motion asked for, nothing moves. */
html.dark-mode-active body {
  isolation: isolate;
  background-image:
    radial-gradient(1px 1px at 66.8% 68.4%, var(--kb-blue-200), transparent),
    radial-gradient(1px 1px at 44.7% 56.2%, var(--kb-blue-200), transparent),
    radial-gradient(1px 1px at 58.1% 38.6%, var(--kb-star-300), transparent),
    radial-gradient(1px 1px at 98.1% 98.9%, var(--kb-blue-200), transparent),
    radial-gradient(1px 1px at 4.9% 33.4%, var(--kb-star-300), transparent),
    radial-gradient(1.5px 1.5px at 50.3% 2.7%, var(--kb-star-100), transparent),
    radial-gradient(1px 1px at 38.7% 72.2%, var(--kb-teal-200), transparent),
    radial-gradient(1.5px 1.5px at 37.8% 26.4%, var(--kb-star-100), transparent),
    radial-gradient(1px 1px at 36.6% 15.9%, var(--kb-star-100), transparent),
    radial-gradient(1px 1px at 44.6% 43.0%, var(--kb-teal-200), transparent),
    radial-gradient(1px 1px at 6.7% 23.8%, var(--kb-star-100), transparent),
    radial-gradient(1.5px 1.5px at 7.0% 24.9%, var(--kb-star-100), transparent),
    radial-gradient(1px 1px at 83.6% 2.7%, var(--kb-star-100), transparent),
    radial-gradient(1px 1px at 20.3% 85.3%, var(--kb-teal-200), transparent),
    radial-gradient(1px 1px at 95.8% 97.5%, var(--kb-star-300), transparent),
    radial-gradient(1px 1px at 73.9% 1.8%, var(--kb-blue-200), transparent),
    radial-gradient(1.5px 1.5px at 82.6% 41.8%, var(--kb-star-300), transparent),
    radial-gradient(1px 1px at 55.3% 72.9%, var(--kb-star-300), transparent),
    radial-gradient(1px 1px at 58.2% 56.5%, var(--kb-star-300), transparent),
    radial-gradient(1.5px 1.5px at 64.4% 95.7%, var(--kb-blue-200), transparent),
    radial-gradient(1px 1px at 80.6% 55.2%, var(--kb-teal-200), transparent),
    radial-gradient(1.5px 1.5px at 84.9% 4.0%, var(--kb-star-300), transparent),
    radial-gradient(1px 1px at 11.7% 46.9%, var(--kb-star-300), transparent),
    radial-gradient(1px 1px at 27.0% 6.6%, var(--kb-blue-200), transparent),
    radial-gradient(1px 1px at 65.3% 40.6%, var(--kb-star-300), transparent),
    radial-gradient(1.5px 1.5px at 97.5% 45.7%, var(--kb-star-100), transparent),
    radial-gradient(1px 1px at 37.2% 73.9%, var(--kb-blue-200), transparent),
    radial-gradient(1px 1px at 64.8% 82.9%, var(--kb-blue-200), transparent),
    radial-gradient(1px 1px at 92.8% 1.2%, var(--kb-star-300), transparent),
    radial-gradient(1.5px 1.5px at 80.3% 29.5%, var(--kb-star-300), transparent),
    radial-gradient(1px 1px at 31.1% 79.1%, var(--kb-blue-200), transparent),
    radial-gradient(1px 1px at 41.8% 74.3%, var(--kb-blue-200), transparent),
    radial-gradient(1px 1px at 27.7% 75.6%, var(--kb-blue-200), transparent),
    radial-gradient(1px 1px at 47.8% 12.4%, var(--kb-star-100), transparent),
    radial-gradient(1px 1px at 47.8% 75.4%, var(--kb-teal-200), transparent),
    radial-gradient(1px 1px at 56.6% 20.4%, var(--kb-teal-200), transparent),
    radial-gradient(1px 1px at 58.7% 38.8%, var(--kb-blue-200), transparent),
    radial-gradient(1px 1px at 72.9% 98.9%, var(--kb-star-100), transparent),
    radial-gradient(1px 1px at 68.2% 97.1%, var(--kb-star-100), transparent),
    radial-gradient(1px 1px at 22.4% 8.4%, var(--kb-star-300), transparent),
    radial-gradient(1px 1px at 82.0% 26.1%, var(--kb-star-100), transparent),
    radial-gradient(1px 1px at 92.3% 99.9%, var(--kb-star-300), transparent),
    radial-gradient(1px 1px at 22.8% 34.7%, var(--kb-blue-200), transparent),
    radial-gradient(1.5px 1.5px at 31.0% 4.1%, var(--kb-star-300), transparent),
    radial-gradient(1px 1px at 69.4% 70.6%, var(--kb-star-100), transparent),
    radial-gradient(1px 1px at 22.1% 38.2%, var(--kb-teal-200), transparent),
    radial-gradient(1.5px 1.5px at 3.3% 38.8%, var(--kb-blue-200), transparent),
    radial-gradient(1px 1px at 31.0% 86.3%, var(--kb-teal-200), transparent),
    radial-gradient(1px 1px at 79.3% 6.6%, var(--kb-blue-200), transparent),
    radial-gradient(1.5px 1.5px at 9.2% 79.4%, var(--kb-star-300), transparent),
    radial-gradient(1px 1px at 37.9% 52.5%, var(--kb-blue-200), transparent),
    radial-gradient(1.5px 1.5px at 34.6% 48.3%, var(--kb-blue-200), transparent),
    radial-gradient(1px 1px at 29.1% 95.5%, var(--kb-teal-200), transparent),
    radial-gradient(1px 1px at 78.5% 43.2%, var(--kb-blue-200), transparent),
    radial-gradient(1px 1px at 5.1% 87.1%, var(--kb-teal-200), transparent),
    radial-gradient(1px 1px at 81.5% 11.5%, var(--kb-star-100), transparent),
    radial-gradient(1.5px 1.5px at 1.2% 0.4%, var(--kb-star-300), transparent),
    radial-gradient(1.5px 1.5px at 99.2% 37.7%, var(--kb-star-300), transparent),
    radial-gradient(1px 1px at 4.8% 2.0%, var(--kb-teal-200), transparent),
    radial-gradient(1px 1px at 24.3% 84.2%, var(--kb-blue-200), transparent),
    radial-gradient(1px 1px at 5.2% 67.6%, var(--kb-blue-200), transparent),
    radial-gradient(1px 1px at 11.6% 31.3%, var(--kb-blue-200), transparent),
    radial-gradient(1px 1px at 49.6% 78.1%, var(--kb-star-100), transparent),
    radial-gradient(1px 1px at 0.7% 4.0%, var(--kb-blue-200), transparent),
    radial-gradient(1px 1px at 5.4% 28.1%, var(--kb-blue-200), transparent),
    radial-gradient(1.5px 1.5px at 76.8% 36.3%, var(--kb-teal-200), transparent),
    radial-gradient(1px 1px at 28.0% 14.0%, var(--kb-star-300), transparent),
    radial-gradient(1px 1px at 41.0% 17.1%, var(--kb-star-100), transparent),
    radial-gradient(1px 1px at 95.3% 4.7%, var(--kb-star-300), transparent),
    radial-gradient(1px 1px at 50.3% 50.7%, var(--kb-blue-200), transparent),
    radial-gradient(1px 1px at 55.6% 98.8%, var(--kb-blue-200), transparent),
    radial-gradient(1.5px 1.5px at 30.4% 22.7%, var(--kb-star-100), transparent),
    radial-gradient(1px 1px at 37.6% 19.4%, var(--kb-star-100), transparent),
    radial-gradient(1px 1px at 14.2% 53.9%, var(--kb-star-100), transparent),
    radial-gradient(1.5px 1.5px at 41.6% 98.2%, var(--kb-star-100), transparent),
    radial-gradient(1px 1px at 3.9% 30.0%, var(--kb-blue-200), transparent),
    radial-gradient(1.5px 1.5px at 74.4% 38.0%, var(--kb-teal-200), transparent),
    radial-gradient(1px 1px at 35.2% 30.3%, var(--kb-teal-200), transparent),
    radial-gradient(1.5px 1.5px at 5.1% 14.7%, var(--kb-star-100), transparent),
    radial-gradient(1px 1px at 65.5% 57.8%, var(--kb-star-300), transparent),
    radial-gradient(1px 1px at 11.6% 70.9%, var(--kb-blue-200), transparent),
    radial-gradient(1px 1px at 29.8% 40.0%, var(--kb-teal-200), transparent),
    radial-gradient(1px 1px at 91.7% 53.4%, var(--kb-star-100), transparent),
    radial-gradient(1px 1px at 91.7% 96.0%, var(--kb-star-300), transparent),
    radial-gradient(1px 1px at 84.5% 78.2%, var(--kb-star-100), transparent),
    radial-gradient(1px 1px at 91.8% 29.6%, var(--kb-star-100), transparent),
    radial-gradient(1.5px 1.5px at 37.2% 23.4%, var(--kb-star-300), transparent),
    radial-gradient(1px 1px at 33.7% 53.3%, var(--kb-blue-200), transparent),
    radial-gradient(1px 1px at 86.1% 89.9%, var(--kb-star-100), transparent),
    radial-gradient(1px 1px at 31.0% 35.1%, var(--kb-star-100), transparent);
  background-attachment: fixed;
}
html.dark-mode-active body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image:
    radial-gradient(2px 2px at 22.3% 32.1%, var(--kb-teal-200), transparent),
    radial-gradient(2px 2px at 24.7% 41.4%, var(--kb-star-300), transparent),
    radial-gradient(2px 2px at 21.1% 10.4%, var(--kb-star-300), transparent),
    radial-gradient(2px 2px at 47.7% 74.4%, var(--kb-teal-200), transparent),
    radial-gradient(2px 2px at 15.9% 59.8%, var(--kb-teal-200), transparent),
    radial-gradient(2px 2px at 6.2% 83.9%, var(--kb-teal-200), transparent),
    radial-gradient(2px 2px at 25.9% 92.7%, var(--kb-star-300), transparent),
    radial-gradient(2px 2px at 31.5% 31.5%, var(--kb-star-100), transparent),
    radial-gradient(2px 2px at 39.6% 44.2%, var(--kb-star-100), transparent),
    radial-gradient(2px 2px at 53.6% 40.8%, var(--kb-blue-200), transparent),
    radial-gradient(2px 2px at 16.5% 6.4%, var(--kb-teal-200), transparent),
    radial-gradient(2px 2px at 95.9% 54.9%, var(--kb-star-100), transparent),
    radial-gradient(2px 2px at 46.9% 14.8%, var(--kb-star-100), transparent),
    radial-gradient(2px 2px at 29.3% 74.7%, var(--kb-star-100), transparent),
    radial-gradient(2px 2px at 4.1% 39.2%, var(--kb-teal-200), transparent),
    radial-gradient(2px 2px at 99.2% 71.6%, var(--kb-star-100), transparent),
    radial-gradient(2px 2px at 65.3% 9.7%, var(--kb-star-300), transparent),
    radial-gradient(2px 2px at 59.9% 13.6%, var(--kb-star-100), transparent);
  animation: kbTwinkle 4.5s ease-in-out infinite alternate;
}
@keyframes kbTwinkle {
  from { opacity: .2; }
  to { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  html.dark-mode-active body::before { animation: none; }
}
.dark-mode-active .kuribaby-topbar {
  border-color: var(--color-border);
}
.dark-mode-active .kuribaby-topbar-right a {
  color: var(--color-text);
}
.dark-mode-active .kuribaby-user {
  color: var(--color-text-secondary);
}
.dark-mode-active .kuribaby-login-hint {
  color: var(--color-text-secondary);
}
.dark-mode-active .kuribaby-footer {
  border-color: var(--color-border);
  color: var(--color-text-secondary);
}
.dark-mode-active body::-webkit-scrollbar-thumb {
  background: var(--color-border-strong);
}
/* Switch at night: checked is the accent track with a dark knob. */
.dark-mode-active .settings-toggle-slider {
  background: var(--color-border-strong);
}
.dark-mode-active .settings-toggle input:checked + .settings-toggle-slider {
  background: var(--color-accent);
}
.dark-mode-active .settings-toggle input:checked + .settings-toggle-slider::before {
  background: var(--color-on-accent);
}

/* Page-level scrollbar follows the synced slim template (kb-base.css), so the
   home page matches the viewer/panel scrollbars. webkit-only on purpose — see
   the warning in kb-base.css about mixing in standard scrollbar properties. */
body::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}
body::-webkit-scrollbar-track {
  background: transparent;
}
body::-webkit-scrollbar-thumb {
  background: var(--color-border-strong);
  border-radius: var(--radius-xs);
}
html:not([data-input="touch"]) body::-webkit-scrollbar-thumb:hover {
  background: var(--color-text-tertiary);
}

/* Root carries the page color too: overscroll/safe-area regions above the body
   otherwise flash default white as a bar at the very top. */
html {
  background: var(--color-bg-page);
}

body {
  margin: 0;
  background: var(--color-bg-page);
  font-family: var(--flairui-font, system-ui, sans-serif);
  color: var(--color-text);
  overflow-x: hidden;   /* full-bleed rules use 50vw, which includes the scrollbar */
}

.kuribaby-page {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 var(--page-gutter);
  box-sizing: border-box;
  /* Full-height column so the footer pins to the viewport bottom on short pages. */
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}

/* The rules (borders) of the top bar and footer bleed edge-to-edge across the
   viewport while their content stays aligned with the centered column. */
.kuribaby-topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-left: calc(50% - var(--kb-half, 50vw));
  margin-right: calc(50% - var(--kb-half, 50vw));
  /* The page's gutter and the grid row's own inset: the bar's last thing stands exactly over the
     grid row's last button. */
  padding: var(--space-m) calc(var(--kb-half, 50vw) - 50% + var(--space-m));
  border-bottom: var(--hairline) solid var(--color-border);
  margin-bottom: var(--space-l);
}

.kuribaby-brand {
  font-family: "EB Garamond", "PingFang SC", "Microsoft YaHei", serif;
  font-size: var(--font-xl);
  font-weight: var(--weight-regular);
  letter-spacing: var(--tracking-caps);
}

.kuribaby-logo {
  display: block;
  color: var(--color-text);
}

.kuribaby-logo svg {
  height: 50px;
  width: auto;
  display: block;
}

@media (max-width: 749px) {
  .kuribaby-logo svg {
    height: 40px;
  }
}

.kuribaby-topbar-right {
  display: flex;
  align-items: center;
  gap: var(--space-m);
  font-size: var(--font-s);
}

.kuribaby-topbar-right a {
  color: var(--color-text);
}

.kuribaby-user {
  color: var(--color-text-secondary);
}

/* Storage used, of the account's allowance (kuribaby-boot.js showUsage). Made hidden and shown
   once the figure arrives. The capsule is the button shell's small one (ui-button ui-button-s);
   here only its words, the small grey of a reading. Here it opens nothing, so the pointer
   passes through it: no hand, no wash. */
.kb-usage-pill[hidden] { display: none !important; }
.kb-usage-pill {
  pointer-events: none;
  font-size: var(--font-xs);
  color: var(--color-text-secondary);
  font-variant-numeric: tabular-nums;
}
/* Near the allowance it is the shell's danger look, the site's red. */
.kb-usage-pill.is-danger { color: var(--color-danger-text); }

.kuribaby-user a {
  color: inherit;
  text-decoration: none;
  pointer-events: none;
}

.kuribaby-login-hint {
  color: var(--color-text-secondary);
  font-size: var(--font-m);
  margin: var(--space-xl) 0;
}

.kuribaby-footer {
  margin-top: auto;   /* pin to the bottom of the full-height page column */
  margin-left: calc(50% - var(--kb-half, 50vw));
  margin-right: calc(50% - var(--kb-half, 50vw));
  /* Shorter bar (user 2026-08-26): was 20px above/below a ~15px line (~56px
     with the border); now 12px each side → ~42px. The line is the body leading, a
     whole number of pixels, so the height does not drift with the font's own. */
  padding: var(--space-m) calc(var(--kb-half, 50vw) - 50%);   /* symmetric above/below the line of text */
  border-top: var(--hairline) solid var(--color-border);
  color: var(--color-text-secondary);
  font-size: var(--font-xs);
  line-height: var(--leading);
  text-align: center;
  letter-spacing: var(--tracking-fine);
}

/* ── Password (cover) page ─────────────────────────────────────────────────
   One photo, cropped by cover-fit: phones take a tall center slice, desktops a
   wide one — no separate crops needed. */
.kb-password-body {
  margin: 0;
  background: var(--kb-star-800);   /* behind the fixed photo cover, never flash white */
}

.kb-password-page {
  position: fixed;
  inset: 0;
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Extra bottom padding biases the centered card 40px above true center. */
  padding: var(--space-xl) var(--space-xl) 100px;
}

.kb-password-card {
  background: transparent;
  width: 320px;
  text-align: center;
}

@media (max-width: 749px) {
  .kb-password-card {
    width: 60vw;
  }
}

/* Full logo in white, floating straight on the photo above the input; the soft
   shadow keeps it readable over bright areas. */
.kb-password-card .kuribaby-logo {
  display: flex;
  justify-content: center;
  margin-bottom: var(--space-xl);
  color: var(--color-on-image);
  filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.35));
}

/* Width-driven: the logo is always 88% of the field width (the desktop ratio,
   282/320) and scales with the card on every screen. Height-driven sizing let
   narrow phone cards squash the SVG to full card width = same width as the
   inputs — the bug that made every "logo size" change a no-op. */
.kb-password-card .kuribaby-logo svg {
  width: 88%;
  height: auto;
}

/* The field wraps the input so the placeholder label can be laid over it. It
   also carries the spacing, so the wrapper box and the input box stay the same
   size — the label is positioned against the wrapper. */
.kb-password-field {
  position: relative;
  margin-bottom: var(--space-m);
}

.kb-password-input {
  width: 100%;
  box-sizing: border-box;
  height: 48px;
  padding: 0 var(--space-l);
  background: rgba(255, 255, 255, 0.14);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  border: var(--hairline) solid var(--color-on-image);
  border-radius: var(--radius-m);
  color: var(--color-on-image);
  /* iOS draws the insertion caret in the system tint (blue) regardless of the
     text color; on this frosted white-on-photo field it should read as white. */
  caret-color: var(--color-on-image);
  /* ONE font-size, ONE padding, in every state. The caret is drawn from the
     ELEMENT's metrics and box, so anything that changes either between empty
     and filled makes the caret jump or resize on the first keystroke. That is
     why the visible placeholder is a separate label rather than ::placeholder
     at a smaller size: the real placeholder is a space, drawing nothing. */
  font-size: var(--font-2xl);
  letter-spacing: var(--tracking-caps);
  margin-bottom: 0;
  font-family: Verdana, sans-serif;
}

/* Put the masking dots' centers on the box midline. No engine exposes where a
   bullet sits inside its line box, and it moves with the platform font, the
   font-size and the box height (48px portrait, 38px landscape) — so a constant
   here can only ever be right on one device. The value is measured from the
   real font metrics on the actual device and written to --kb-dot-shift-*
   (see password.liquid); it stays 0 until then. Applied in EVERY state so the
   caret never moves: with the placeholder gone from the input, nothing else
   depends on it. */
.kb-password-input {
  padding-bottom: var(--kb-dot-shift-bottom, 0px);
  padding-top: var(--kb-dot-shift-top, 0px);
}

/* The visible placeholder. Centred by the layout — inset to the input's box and
   flex-centered — so it needs no font metrics and lands identically on every
   platform. The transparent border and matching padding reproduce the input's
   text origin exactly. */
.kb-password-ph {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  box-sizing: border-box;
  border: var(--hairline) solid transparent;
  padding: 0 var(--space-l);
  font-size: var(--font-l);
  font-family: var(--flairui-font, system-ui, sans-serif);
  color: var(--color-on-image);
  pointer-events: none;
}

/* Clears on focus, and stays gone while there is a value. */
.kb-password-input:focus ~ .kb-password-ph,
.kb-password-input:not(:placeholder-shown) ~ .kb-password-ph {
  opacity: 0;
}

/* Focus = the white line brightens to full, no layout shift. */
.kb-password-input:focus {
  outline: none;
  border-color: var(--color-on-image);
  box-shadow: inset 0 0 0 0.2px var(--color-on-image);
}

/* Touch devices keep Verdana on purpose. Handing the dots to the system font
   looked like the native choice, but measured on an iPhone its bullet sits
   3.05px BELOW the line box center — and the only lever for that is padding,
   which moves the caret by the same amount, so centring the dots pushed the
   caret off instead. Verdana's bullet is very close to centered, so almost no
   correction is needed and the caret stays where it belongs. Size and tracking
   still shrink for the narrower box. */
@media (hover: none) and (pointer: coarse) {
  .kb-password-input {
    font-size: var(--font-xl);
    letter-spacing: var(--tracking-caps);
  }
}

.kb-password-btn {
  width: 100%;
  box-sizing: border-box;
  height: 48px;
  padding: 0 var(--space-m);
  background: var(--color-on-image);
  border: none;
  border-radius: var(--radius-m);
  color: var(--kb-star-950);
  font-size: var(--font-l);
  font-family: inherit;
  cursor: pointer;
  transition: background var(--motion);
}

.kb-password-btn:hover,
.kb-password-btn:active {
  background: var(--kb-star-100);
}

/* Phone landscape. MUST come after every base password rule above: same
   specificity, so file order decides — this block sitting earlier in the file
   was silently dead for its entire life (the original landscape bug). */
@media (max-height: 620px) and (orientation: landscape) {
  .kb-password-page {
    /* Extra bottom padding biases the card 30px above true center. */
    padding: var(--space-m) var(--space-m) 72px;
  }
  .kb-password-card {
    width: auto;
  }
  /* Gap follows the desktop RATIO (22/320 of field width -> 17px at 250px). */
  .kb-password-card .kuribaby-logo {
    margin-bottom: var(--space-l);
  }
  .kb-password-card .kuribaby-logo svg {
    width: 200px;
    height: auto;
  }
  /* The field, not the input, carries the narrow width here: the placeholder
     label is positioned against the wrapper, so the two boxes must coincide. */
  .kb-password-field,
  .kb-password-btn {
    display: block;
    width: 250px;
    margin-left: auto;
    margin-right: auto;
  }
  .kb-password-input,
  .kb-password-btn {
    height: 38px;
  }
  .kb-password-input {
    width: 100%;
  }
  .kb-password-field {
    margin-bottom: var(--space-s);
  }
}

/* Shopify's default_errors renders a <ul>. With the card centered, the browser's
   list marker stays pinned at the list's left edge (plus its 40px padding) while
   the text centers inside the full-width row — that is the wide gap between the
   dot and the message. Drop the real marker and draw the dot inline, hard in
   front of the text, so the pair centers as one unit.
   The marker and the padding belong to the <ul> INSIDE div.errors, not to the
   div: the browser gives every ul its own list-style disc and 40px padding,
   so none on the div never reached the list, and the real marker showed beside
   the inline dot (owner, 2026-09-15: two dots, far apart). */
.kb-password-card .errors {
  color: var(--kb-sunrise-300);
  font-size: var(--font-s);
  margin: 0 0 var(--space-m);
  padding: 0;
}

.kb-password-card .errors ul {
  list-style: none;
  margin: 0;
  padding: 0;
}

.kb-password-card .errors li::before {
  content: "\2022";
  margin-right: var(--space-s);
}

/* The site's way in (templates/page.enter.liquid): the password page's field and button, with
   letters typed in the label's face, not the dots' Verdana. After the landscape block: it wins. */
.kb-enter .kb-enter-input,
.kb-enter .kb-enter-input:focus {
  font-family: var(--flairui-font, system-ui, sans-serif);
  font-size: var(--font-l);
  letter-spacing: normal;
  padding-top: 0;
  padding-bottom: 0;
}

.kb-enter .kb-enter-google {
  display: flex;
  align-items: center;
  justify-content: center;
  text-decoration: none;
}

.kb-enter-or,
.kb-enter-note {
  margin: var(--space-m) 0;
  color: var(--color-on-image);
  font-size: var(--font-s);
  overflow-wrap: anywhere;
}

.kb-enter-note {
  margin-top: 0;
}

.kb-enter .kb-password-btn:disabled {
  opacity: var(--dim);
  cursor: default;
}

.kb-enter-back {
  margin-top: var(--space-m);
  padding: var(--space-s);
  background: none;
  border: none;
  color: var(--color-on-image);
  font-size: var(--font-s);
  font-family: inherit;
  cursor: pointer;
}

/* Uploading: the ring drawn as iOS draws an app that is downloading (owner,
   2026-09-15), from DAProgressOverlayView's drawInContext. Kuribaby's own copy,
   overriding the frozen platform build's [data-uploading] ring (same
   selectors, loaded later):
   - the whole icon is dimmed;
   - a band from 0.6 to 0.7 of half the short side is CLEAR, a hole in the
     dimming showing the icon itself;
   - inside 0.6 the dimming is cleared as a sector from 12 o'clock, clockwise,
     as far as the upload has gone;
   - beginning, both radii grow from 0 in 1/3 s; finishing (data-upload-done,
     kuribaby-boot.js finishRing) both grow on to the corners,
     max(w, h) / sqrt(2), in 0.4 s. Both ease in and out.
   The hole is composed bottom to top: full - ((sector ∩ disc) ∪ ring).
   The disc reaches a pixel PAST the ring's inner edge on purpose: butting the
   two soft edges together left a hairline arc between the sector and the ring,
   because mask-composite add is source-over rather than a sum — where each
   ramp is half opaque the union is 0.5 + 0.5x0.5 = 0.75 and a quarter of the
   dimming survives (measured gray 223 where a hole is 255; owner, 2026-09-17).
   Nothing here needs the platform: this store dims, counts and finishes on its
   own, and the ring draws the same with the platform build absent. */
@property --kb-upload-inner { syntax: "<percentage>"; inherits: false; initial-value: 60%; }
@property --kb-upload-outer { syntax: "<percentage>"; inherits: false; initial-value: 70%; }
@keyframes kb-upload-begin {
  from { --kb-upload-inner: 0%; --kb-upload-outer: 0%; }
}
/* The layer itself, so this block stands on its own: with the synced platform
   stylesheet gone there would otherwise be no ::before to dim with. */
[data-uploading] { position: relative; pointer-events: none; }
[data-uploading]:not(img):not(video)::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  z-index: 4;
  pointer-events: none;
  /* This store's own dimming, not the frozen build's backdrop-filter: a layer
     of black with the holes masked out of it, the way iOS draws it, so through
     a hole there is no layer at all and the icon's own pixels show. It is also
     the only form that survives a rounded tile — Chrome dims the whole tile
     and ignores the mask when the dimming is a backdrop-filter. The black is the
     library's dimming (follows flair.design bb6162e). */
  background: var(--scrim);
  -webkit-backdrop-filter: none;
          backdrop-filter: none;
  --kb-upload-inner: 60%;
  --kb-upload-outer: 70%;
  /* This store writes its own percentage (kuribaby-boot.js uploadRing.pct).
     The platform's variable is read only as a fallback, so the ring draws
     whether or not anything of the platform's is loaded (owner, 2026-09-17). */
  --kb-upload-sweep: calc(var(--kb-upload-pct, var(--flairui-upload-pct, 0)) * 3.6deg);
  -webkit-mask-image:
    linear-gradient(#000, #000),
    radial-gradient(circle closest-side,
      transparent calc(var(--kb-upload-inner) - 0.5px), #000 calc(var(--kb-upload-inner) + 0.5px),
      #000 calc(var(--kb-upload-outer) - 0.5px), transparent calc(var(--kb-upload-outer) + 0.5px)),
    radial-gradient(circle closest-side,
      #000 calc(var(--kb-upload-inner) + 0.5px), transparent calc(var(--kb-upload-inner) + 1.5px)),
    conic-gradient(from 0deg,
      #000 calc(var(--kb-upload-sweep) - 1.2deg),
      transparent var(--kb-upload-sweep));
  mask-image:
    linear-gradient(#000, #000),
    radial-gradient(circle closest-side,
      transparent calc(var(--kb-upload-inner) - 0.5px), #000 calc(var(--kb-upload-inner) + 0.5px),
      #000 calc(var(--kb-upload-outer) - 0.5px), transparent calc(var(--kb-upload-outer) + 0.5px)),
    radial-gradient(circle closest-side,
      #000 calc(var(--kb-upload-inner) + 0.5px), transparent calc(var(--kb-upload-inner) + 1.5px)),
    conic-gradient(from 0deg,
      #000 calc(var(--kb-upload-sweep) - 1.2deg),
      transparent var(--kb-upload-sweep));
  -webkit-mask-size: 100% 100%;
          mask-size: 100% 100%;
  -webkit-mask-position: center;
          mask-position: center;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  mask-composite: subtract, add, intersect, add;
  animation: kb-upload-begin var(--motion-slow) cubic-bezier(0.42, 0, 0.58, 1);
}
/* A full sector has no edge to soften: a stop at 360deg leaves a hairline
   wedge at 12 o'clock. And at 100% the dimming stays until the save is done;
   the frozen build faded it out the moment the bytes were sent. */
[data-uploading][data-upload-pct="100"]:not(img):not(video)::before {
  --kb-upload-sweep: 362deg;
  opacity: 1;
}
[data-uploading][data-upload-done]:not(img):not(video)::before {
  --kb-upload-inner: var(--kb-upload-open, 142%);
  --kb-upload-outer: var(--kb-upload-open, 142%);
  transition: --kb-upload-inner var(--motion-slow) cubic-bezier(0.42, 0, 0.58, 1), --kb-upload-outer var(--motion-slow) cubic-bezier(0.42, 0, 0.58, 1);
}
@media (prefers-reduced-motion: reduce) {
  [data-uploading]:not(img):not(video)::before { animation: none; }
  [data-uploading][data-upload-done]:not(img):not(video)::before {
    --kb-upload-inner: 60%;
    --kb-upload-outer: 70%;
    opacity: 0;
    transition: opacity var(--motion) linear;
  }
}

/* Press/hover feedback lives ONLY on the rounded tile, never the whole card:
   kill the mobile tap-highlight overlay and any card-level background. */
.studio-portfolio-card,
.studio-portfolio-card * {
  -webkit-tap-highlight-color: transparent;
}
.studio-portfolio-card,
html:not([data-input="touch"]) .studio-portfolio-card:hover,
.studio-portfolio-card:active {
  background: none !important;
}

/* Card file names: two lines with roomier leading. The synced panel inlines a
   one-line ellipsis style on these spans, so !important is required to win. */
.studio-portfolio-name {
  white-space: normal !important;
  display: -webkit-box !important;
  -webkit-box-orient: vertical !important;
  -webkit-line-clamp: 2 !important;
  overflow: hidden !important;
  line-height: var(--leading) !important;
  max-width: 100% !important;
  /* A name with no spaces wraps inside its column rather than widening it. */
  overflow-wrap: anywhere !important;
}

/* Card-size toggle: large mode, modeled on the upstream image page (fixed column
   count, square tiles that fill their cell and scale with the window). Small mode
   keeps the synced dense 48px layout. */
#studioPortfolioGrid.kb-cards-large .studio-pf-folder-body {
  grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)) !important;
  gap: var(--space-l) var(--space-m) !important;
}
/* Springboard tiles (iPhone-home-screen style). Two modes: default and toggled
   (.kb-cards-large = bigger tiles). Phone: fluid — 4 per row default, 3 per row
   toggled. Desktop: SAME PIXEL SIZE as the phone tiles (~80px / ~112px), fitting
   as many per row as the width allows — not the same per-row count. */
#studioPortfolioGrid .studio-portfolio-card {
  width: 100%;
}
#studioPortfolioGrid .studio-portfolio-thumb {
  border-radius: var(--radius-tile) !important;
  position: relative !important;
}
#studioPortfolioGrid .studio-portfolio-thumb img,
#studioPortfolioGrid .studio-portfolio-thumb.is-app-icon > svg {
  position: absolute !important;
  inset: 0 !important;
  width: 100% !important;
  height: 100% !important;
  object-fit: cover;
}
#studioPortfolioGrid .studio-portfolio-thumb:not(.is-app-icon) > svg {
  width: 40% !important;
  height: auto !important;
}

/* Phone: the proportions of an iPhone home screen (owner, 2026-09-14). The
   columns meet with no gap and each tile takes part of its column, so the
   space between tiles comes from the columns themselves and a name has the
   whole column to wrap in: 4 across, a tile 72% of its column (about 66px
   with 25px between in the grid's 366px on a 430px phone; 64% read small,
   owner 2026-09-14); 3 across, 79%. The two rows span the same width, first
   tile's left edge to last tile's right edge (owner, 2026-09-14): a row
   leaves half a column's spare on each side, (1 - 0.72) / 4 / 2 = 3.5% of the
   grid for 4 across, so 3 across takes (1 - 0.79) / 3 / 2 = 3.5% too, and the
   space between tiles comes out equal as well. */
@media (max-width: 749px) {
  #studioPortfolioGrid .studio-pf-folder-body {
    /* minmax(0, 1fr), never bare 1fr: a 1fr column cannot be narrower than
       its longest unbreakable word, so one name without spaces
       ("Export_10_Blank.3dm") widened its column and pushed the row past the
       screen (owner, 2026-09-15). The name breaks inside a word instead
       (.studio-portfolio-name above). */
    grid-template-columns: repeat(4, minmax(0, 1fr)) !important;
    gap: var(--space-xl) 0 !important;   /* rows on the spacing steps (follows flair.design 7d7f303) */
  }
  #studioPortfolioGrid.kb-cards-large .studio-pf-folder-body {
    grid-template-columns: repeat(3, minmax(0, 1fr)) !important;
    gap: var(--space-2xl) 0 !important;
  }
  #studioPortfolioGrid .studio-portfolio-thumb {
    width: 72% !important;
    height: auto !important;
    aspect-ratio: 1 / 1 !important;
  }
  #studioPortfolioGrid.kb-cards-large .studio-portfolio-thumb {
    width: 79% !important;
  }
}

@media (min-width: 750px) {
  #studioPortfolioGrid .studio-pf-folder-body {
    grid-template-columns: repeat(auto-fill, 80px) !important;
    gap: var(--space-xl) var(--space-xl) !important;
    justify-content: space-between !important;
  }
  #studioPortfolioGrid.kb-cards-large .studio-pf-folder-body {
    grid-template-columns: repeat(auto-fill, 112px) !important;
    gap: var(--space-2xl) var(--space-2xl) !important;
  }
  #studioPortfolioGrid .studio-portfolio-thumb {
    width: 80px !important;
    height: 80px !important;
  }
  #studioPortfolioGrid.kb-cards-large .studio-portfolio-thumb {
    width: 112px !important;
    height: 112px !important;
  }
}

/* A stack of files is a tile too (kb-studio-portfolio-panel.js: its paper layers and its top
   card sit in .studio-pf-stack, 48px in the platform's sheet). The stack takes the tile's size
   and its top card fills it, so the layers show only as the paper's edges at its right and
   bottom, in the tile's corner. Without this the top card was 72% of the 48px stack on a phone,
   smaller than the paper behind it (owner, 2026-10-03: "why the double image"), and 80px over
   it on a computer, hiding it. */
@media (max-width: 749px) {
  #studioPortfolioGrid .studio-pf-stack { width: 72%; height: auto; aspect-ratio: 1 / 1; }
  #studioPortfolioGrid.kb-cards-large .studio-pf-stack { width: 79%; }
}
@media (min-width: 750px) {
  #studioPortfolioGrid .studio-pf-stack { width: 80px; height: 80px; }
  #studioPortfolioGrid.kb-cards-large .studio-pf-stack { width: 112px; height: 112px; }
}
#studioPortfolioGrid .studio-pf-stack > .studio-pf-stack-top { width: 100% !important; height: 100% !important; }
#studioPortfolioGrid .studio-pf-stack-layer { border-radius: var(--radius-tile); }

/* Wide desktop: fixed counts (10 small / 8 large per row), edges flush. The
   specified gap is the MINIMUM; space-between only distributes what's left
   beyond it, so shrinking the window can never squeeze tiles together. The
   1184px breakpoint is exactly where auto-fill below it naturally lands on 10
   per row too, so crossing the line never changes the column count (no jump). */
@media (min-width: 1184px) {
  #studioPortfolioGrid .studio-pf-folder-body {
    grid-template-columns: repeat(10, 80px) !important;
    justify-content: space-between !important;
    gap: var(--space-xl) var(--space-xl) !important;
  }
  #studioPortfolioGrid.kb-cards-large .studio-pf-folder-body {
    grid-template-columns: repeat(8, 112px) !important;
    gap: var(--space-2xl) var(--space-2xl) !important;
  }
}
/* Card file names keep the large-mode size in BOTH modes (user 2026-08-26):
   toggling 4-grid/9-grid changes the tiles only, never the text. */
#studioPortfolioGrid .studio-portfolio-card .studio-portfolio-name {
  font-size: var(--font-xs) !important;
}

/* Single-category deployment: one level only. The per-folder header row (name,
   count, menu) is hidden; its lone menu action is re-hosted in the section menu
   by kuribaby-boot.js. */
.studio-pf-folder-head {
  display: none !important;
}

/* Modal close buttons match the other icon buttons' gray. */
.project-modal-close {
  color: var(--color-text-secondary) !important;
}

/* 3D viewer: the close button follows the same color as the rest of the toolbar. */
.flairui-viewer-close {
  color: var(--color-text) !important;
}

/* Viewer close glyph, tuned down from the 14px baseline (18px on touch). */
.flairui-viewer-close svg {
  width: 12px !important;
  height: 12px !important;
}
@media (hover: none) and (pointer: coarse) {
  .flairui-viewer-close svg {
    width: 16px !important;
    height: 16px !important;
  }
}

/* Menu icons are sized so their INK matches, not their boxes. Measured as a
   fraction of each icon own viewBox — which is NOT always 24: share is drawn
   on 26 and rename on 21.4, and measuring those against 24 reports them small
   and invites a fix for nothing. The median fill is 75%, and download, trash,
   show, rename and tag all sit on it, so 24px is the baseline size.

   Two miss it and get a size that lands their ink on the same 18 units:
     zap    fills 83% -> 24 * 75/83 = 21.5px
     share  fills 69% -> 24 * 75/69 = 26px

   The download glyph used to carry a 22px exception. Its ink is exactly on the
   baseline, so that exception was making it 8% SMALLER than its neighbours —
   the opposite of what it was for. Removed. */
.action-icon svg.kb-icon-zap {
  width: 21.5px !important;
  height: 21.5px !important;
}
.action-icon svg.kb-icon-share {
  width: 26px !important;
  height: 26px !important;
}

/* Header action icons (upload / import / select / sort, rename and share while a
   collection is selected, the three-dot overflow on phones) are the shared
   button shell (ui-button is-icon is-quiet), drawn by the pinned shells, icon
   and all; this page only says which of them show. The synced dots button
   stays in the DOM for the panel's wiring but is hidden. Rename and share exist
   only while a collection is selected, and the shell's own display would
   defeat the hidden attribute boot toggles. */
#kbRenameBtn[hidden],
#kbShareBtn[hidden] {
  display: none;
}
/* The grid-size switch's icons match the viewer toolbar's 18px baseline
   (.flairui-preview-tool svg; user 2026-08-26). */
.kb-size-btn svg {
  width: 18px;
  height: 18px;
  display: block;
}

/* ── Series bar ───────────────────────────────────────────────────────────
   The controls row under the standalone title: series chips left, the grid's
   controls (night, size, upload, select) right. One row for everything you act
   on — chips off on their own row read as belonging to nothing. Chips scroll
   sideways inside their slot rather than wrapping: a wrapping bar changes
   height as series are added and shoves the grid down. */
.kb-series-bar {
  display: flex;
  align-items: center;
  gap: var(--space-m);
  /* Real air below: the grid sat almost against this row. */
  padding: var(--space-2xs) var(--space-m) var(--space-l);
}
.kb-series-chips {
  display: flex;
  align-items: center;
  gap: var(--space-s);
  /* The pill's height, so the row is as tall without it. */
  min-height: var(--control-m);
  flex: 1 1 auto;
  min-width: 0;
  overflow-x: auto;
  overflow-y: hidden;
  white-space: nowrap;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.kb-series-chips::-webkit-scrollbar { display: none; }
/* The grid's own control, at the left end of the row — the size toggle changes
   the grid, not a selection, so it sits with the grid rather than in the
   cluster of actions on the right, where the platform keeps it too. */
.kb-series-left {
  display: inline-flex;
  align-items: center;
  flex: 0 0 auto;
}
/* Card size as a pair: both densities on screen, the current one lit in the
   row's own gray and the other one lighter. The pair sits in the same 24px
   boxes as the buttons on the right, so the row reads as one line of controls
   even though this end changes the grid and that end acts on a selection. */
.kb-size-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
}
.kb-series-left .kb-size-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border-radius: var(--radius-s);
  color: var(--color-text-tertiary);
  transition: color var(--motion);
}
.kb-series-left .kb-size-btn.is-active {
  color: var(--color-text-secondary);
}
html:not([data-input="touch"]) .kb-series-left .kb-size-btn:not(.is-active):hover {
  color: var(--color-text-secondary);
}
.kb-series-right {
  display: inline-flex;
  align-items: center;
  gap: var(--space-s);
  flex: 0 0 auto;
  margin-left: auto;
}

/* The size switch's buttons used to sit inside .flairui-bar-section-header, whose
   synced reset (background/border none, padding zeroed) stripped the UA button
   chrome; out on this row the slot carries the same reset itself. The right
   slot's buttons are shells and draw themselves. */
.kb-series-left button {
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  color: inherit;
}

/* The series pill is the shared button (ui-button), the outline look whether or not a series is
   chosen (user 2026-08-26): the label already says which series is active. */

/* Prev/next pinned to the RIGHT EDGE of the title slot, not to the text: the
   nav segment's width comes from the top bar's three-段 layout, never from how
   long the file name is, so the arrows hold one fixed position on every device.
   A long name ellipsizes inside the slot (the crumbs already shrink, min-width
   0 + ellipsis) and can never push the arrows; the padding is the guaranteed
   minimum gap for when the auto margin collapses to zero. */
.kb-viewer-filenav {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  margin-left: auto;
  padding-left: var(--space-m);
  flex: 0 0 auto;
}

/* The viewer already disables the arrow at either end (showFile writes
   .disabled on every switch, including the first), and a disabled button fires
   nothing — but these custom icon buttons had no :disabled rule, so nothing
   ever LOOKED disabled. */
.kb-viewer-filenav button:disabled {
  opacity: var(--dim);   /* the one dim value (follows flair.design dcd349c) */
  cursor: default;
  pointer-events: none;
}

/* ── Fixed chrome: only the files scroll ─────────────────────────────────
   The page column locks to the viewport and stops scrolling as a document;
   the grid becomes the one scroll container. Topbar, Portfolio title, the
   controls row and the footer all hold still. The synced grid rule only sets
   display/padding, so taking over its overflow is safe. (Briefly switched to
   document scroll on 2026-08-26; the user preferred the pinned footer, just
   shorter — see .kuribaby-footer padding.) */
/* Clipped top and bottom only: the top bar's and the footer's lines bleed out of the column
   sideways, to the sidebar and the window's edge (clip keeps the other axis visible; hidden would not). */
.kuribaby-page {
  height: 100vh;
  height: 100dvh;
  overflow-x: visible;
  overflow-y: clip;
}
.studio-portfolio-section {
  /* Basis 0, NOT auto: an auto basis claims the grid's full content height
     first, so taller content (the large-tile mode) overdraws the viewport and
     the negative space is shaved off every sibling — the topbar and the title
     row visibly rode UP a few pixels whenever the grid grew. From a zero basis
     the section only ever takes the free space, and nothing above it moves. */
  flex: 1 1 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
/* The rows that must never give up a pixel to flex negotiation. */
.kuribaby-topbar,
.kuribaby-footer,
.kb-series-bar {
  flex-shrink: 0;
}
.studio-portfolio-grid {
  flex: 1 1 0;   /* zero basis here too — same reasoning as the section */
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}

/* The three dots stay on the row at every width: on a desktop their menu holds
   only what has no button there, the recycle bin (owner, 2026-10-04, as on
   flair.design). */
/* Phones: the row cannot fit six controls beside the collection dropdown — the
   night switch stays out (a switch inside a menu is two taps just to see it),
   the rest fold into the three-dot menu boot builds from the real buttons. */
@media (max-width: 768px) {
  /* The size toggle stays on the row (user keeps it at hand); the rest fold.
     Import folds with them — it is in the overflow menu's list, and left
     visible here it appeared twice. */
  #kbRenameBtn,
  #kbShareBtn,
  #kbUploadBtn,
  #kbImportSetBtn,
  #kbSelectBtn,
  #kbSortBtn {
    display: none !important;
  }
}
/* The grid scrollbar hugs the page's right edge instead of floating at the
   content column's inset: undo the page padding for the scroller only, and
   hand it back as padding so the cards do not move. */
.studio-portfolio-grid {
  margin-right: calc(-1 * var(--page-gutter));
  padding-right: var(--page-gutter);
  /* The 4/9 toggle changes whether the grid overflows its fixed height, so the
     scrollbar would blink in and out and shove every card sideways by its
     width on each tap. Reserve the gutter permanently: the columns hold still
     whichever size is showing. */
  scrollbar-gutter: stable;
}

/* Overflow-menu icons: ONE size for all of them (user 2026-08-26 — uniform
   beats optical here), overriding the synced menu baseline that forces 24px
   with !important on every menu svg. */
.kb-mm-kbUploadBtn,
.kb-mm-kbSelectBtn,
.kb-mm-kbRenameBtn,
.kb-mm-kbShareBtn,
.kb-mm-kbSortBtn,
.kb-mm-kbImportSetBtn {
  width: 14px !important;
  height: 14px !important;
}
/* The sort menu: the action menu's rows, label left and tick right. */
.kb-sort-menu .project-menu-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-l);
  white-space: nowrap;
}
.kb-sort-tick {
  display: inline-flex;
  align-items: center;
  line-height: 0;
  width: 14px;
}
/* A menu's question above its choices: read, not tapped. */
.kb-menu-heading {
  padding: var(--space-s) var(--space-m) var(--space-xs);
  font-size: var(--font-s);
  color: var(--color-text-secondary);
  white-space: nowrap;
  pointer-events: none;
}

/* The dropdown trigger: label plus the design-system chevron. The label gets
   the ellipsis so a long collection name truncates instead of clipping. */
/* No double-tap zoom anywhere in the app chrome (user 2026-08-26: two quick
   taps on the series pill zoomed the whole page). `manipulation` keeps pan and
   pinch, removes only the double-tap gesture — no viewport lockdown needed. */
.kuribaby-page,
.kuribaby-page * {
  touch-action: manipulation;
}
.kb-series-select {
  flex: 0 0 auto;
  max-width: 100%;
  min-width: 0;
}
.kb-series-select[hidden] { display: none; }
.kb-series-select-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ── Viewer: products dropdown ────────────────────────────────────────────
   A collection's files, offered beside the prev/next arrows (user 2026-08-27). */
.kb-viewer-products-menu {
  max-height: 60vh;
  overflow-y: auto;
}
.kb-viewer-products-menu .project-menu-item.is-active {
  font-weight: var(--weight-medium);   /* the heaviest face the site ships: never a weight the font lacks */
}
/* Phones: Products and Layers live in the viewer's ⋯ menu (boot adds the rows
   and this class), so their bar buttons go — the file name keeps the width. */
.kb-phone-tools .kb-viewer-products,
.kb-phone-tools .flairui-viewer-actions .kb-viewer-layers {
  display: none !important;
}
@media (max-width: 768px) {
  .kb-viewer-filenav { padding-left: var(--space-s); }
}
/* Phone title = the PRODUCT, not the series (user 2026-08-27). The synced rule
   keeps only the parent crumb on phones (its folder use case); here the arrows
   and the Products list switch products, so the name must follow the switch.
   Desktop keeps "series › product". Same media query as the synced rule so the
   override wins by source order. */
@media (max-width: 768px), (max-height: 500px) {
  .flairui-viewer-nav .flairui-viewer-crumb:not(.is-current),
  .flairui-viewer-nav .flairui-viewer-crumb-sep { display: none; }
  .flairui-viewer-nav .flairui-viewer-crumb.is-current { display: inline; }
}
/* Press-and-drag reorder must not start a text selection on the file names
   (user 2026-08-27): the whole card is a drag handle, never selectable text. */
#studioPortfolioGrid .studio-portfolio-card,
#studioPortfolioGrid .studio-portfolio-name {
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}
/* The cube glyph reads 1px small beside the arrows, same as the layers glyph. */
.kb-viewer-products svg {
  width: 19px !important;
  height: 19px !important;
}
/* "Loading preview…" gets the upstream's text-level shimmer (user 2026-08-27):
   one light band flowing left→right through the letters. Same gradient stops,
   clip and 100%→0% travel as the source — going past 0–100% enters the tiled
   repeat and reads as several bands flickering. */
.flairui-preview-loading .flairui-preview-hint {
  width: fit-content;
  background-image: linear-gradient(90deg, var(--color-text-tertiary) 42%, var(--kb-star-200) 50%, var(--color-text-tertiary) 58%);
  background-size: 200% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  animation: kbLoadingShimmer 1.8s linear infinite;
}
.dark-mode-active .flairui-preview-loading .flairui-preview-hint {
  background-image: linear-gradient(90deg, var(--color-text-secondary) 42%, var(--kb-star-200) 50%, var(--color-text-secondary) 58%);
}
@keyframes kbLoadingShimmer {
  0%   { background-position: 100% 0; }
  100% { background-position: 0% 0; }
}
/* Picker's primary button while nothing is selected: the synced disabled
   color (--color-shadow) is near-invisible here, so the button read as
   missing (user 2026-08-27). A clear mid gray says "there, not yet". */
.project-modal-btn-primary:disabled {
  background: var(--kb-star-300);
  color: var(--kb-star-0);
}
.dark-mode .project-modal-btn-primary:disabled {
  background: var(--kb-star-800);
  color: var(--kb-star-500);
}
/* iOS (WeChat WebView): dragging the model showed a capsule-shaped highlight
   under the finger (user 2026-08-27) — the WebView's tap highlight / focus ring
   on the stage. No highlight, callout, selection or focus ring anywhere on the
   3D stage; the gesture belongs to the renderer. */
.flairui-viewer-stage,
.flairui-viewer-stage *,
.flairui-viewer-stage canvas {
  -webkit-tap-highlight-color: transparent;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
  outline: none !important;
}
.flairui-viewer-stage canvas {
  touch-action: none;
}
/* ...except anything typed into on the stage (material search): an ancestor
   user-select:none locks input text in Safari (synced CSS warns about this). */
.flairui-viewer-stage input,
.flairui-viewer-stage textarea,
.flairui-viewer-stage [contenteditable="true"] {
  -webkit-user-select: text;
  user-select: text;
}
/* The viewer's rail follows flair.design 23bfdc2 (owner, A on mut83fgo). A row of text, a 3D
   layer, is the list-row shell's row: Flair gives it the ui-row class, but here the row is built by
   the platform's frozen preview module (kb-file-preview.js), which cannot change, so the shell's
   row figures are given to it from the same tokens. Its hover already answers a pointer only; a
   finger leaves no grey on it either. A row with a thumbnail keeps its own frame. */
.flairui-viewer-rail .flairui-preview-rail-item:not(.has-thumb) {
  gap: var(--space-s);
  min-height: var(--control-l);
  padding: 0 var(--space-m);
  border-radius: var(--radius-s);
  font-size: var(--font-m);
  line-height: var(--leading-tight);
}
html[data-input="touch"] .flairui-viewer-rail .flairui-preview-rail-item:not(.has-thumb):active { background: transparent; }
/* The eye's hover corner is the icon buttons', and the colour dot's edge the pressable-control line.
   Flair's dot wears the dot shell (.ui-dot.is-swatch, .is-btn, shells v7.03); the frozen module
   gives this one neither class, so its edge and its ring of hover (--space-xs) are the shell's
   figures, written here once. */
.flairui-preview-rail-eye { border-radius: var(--radius-icon-btn); }
/* A hidden layer's eye is the library's faint grey, not a fade (follows flair.design 02f9516; the frozen
   module still says opacity .3, and this sheet loads after it). */
.flairui-preview-rail-eye.is-off { opacity: 1; color: var(--color-text-tertiary); }
.flairui-preview-rail-dot { box-shadow: inset 0 0 0 var(--hairline) var(--color-border-strong); }
html:not([data-input="touch"]) .flairui-preview-rail-dot.is-btn:hover,
.flairui-preview-rail-dot.is-btn:active { box-shadow: inset 0 0 0 var(--hairline) var(--color-border-strong), 0 0 0 var(--space-xs) var(--color-bg-hover); }
/* The viewer's last numbers, on the scale as flair.design's are (0ea5626): the gap in the top
   bar's nav, and a tool button's face, which is the page's and not the browser's. */
.flairui-viewer-nav { gap: var(--space-xs); }
.flairui-preview-tool { font: inherit; }
/* On a phone the Layers sheet's top fade and the room under it are the sheet's own (kb-sheet.css):
   opaque past the close's foot, and the rows start a step below where it is fully transparent. */
@media (max-width: 768px), (max-height: 500px) {
  .flairui-viewer-rail:not(.is-thumbs)::before { height: var(--flairui-sheet-fade); }
  .flairui-viewer-rail:not(.is-thumbs) .flairui-viewer-rail-body { padding-top: var(--flairui-sheet-below-fade); }
}

/* When a file cannot be previewed: one tile, in the viewer and in a piece's details dialog,
   over the frozen registry's 72px base (follows flair.design e7f9a63). The grid tile's corner at
   this size; its glyph takes the share a grid tile's glyph takes (flair.design 4f3b0e9), and an
   app icon fills it, as in the grid (flair.design 76ba9bb). */
.flairui-preview-icon {
  width: 120px;
  height: 120px;
  border-radius: var(--radius-tile);
}
.flairui-preview-icon:not(.is-app-icon) svg {
  width: 40%;
  height: auto;
}
/* Viewer fail state (e.g. a member too large for a phone): the tile above, one
   line of text below (user 2026-08-27). A piece's details dialog shows the
   same note (follows flair.design b0cf361). */
.flairui-viewer-stage:has(> .flairui-preview-icon) {
  flex-direction: column;
  gap: var(--space-l);
}
/* A kind without a mark of its own is the plain tile here too (kuribaby-boot.js KuribabyTiles).
   The frozen registry writes its old colour inline on this tile, so only !important reaches it. */
.flairui-viewer-stage.is-plain-kind > .flairui-preview-icon,
.studio-pf-preview-wrap.is-plain-kind > .flairui-preview-icon {
  background: var(--color-bg-surface) !important;
  color: var(--color-text);
}
.flairui-viewer-stage > .flairui-preview-hint,
.studio-pf-preview-wrap > .flairui-preview-hint {
  font-size: var(--font-m);
  line-height: var(--leading);
}
.flairui-viewer-stage > .flairui-preview-hint {
  max-width: 280px;
  padding: 0 var(--space-l);
}

/* The collection chooser: existing collections as rows, typing reserved for
   a new one. Scrolls at a height that still shows the buttons below it. */
.kb-coll-list {
  max-height: 46vh;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  margin: var(--space-xs) 0 var(--space-m);
}
.kb-coll-row { cursor: pointer; }   /* the list row shell (ui-row) draws it */
/* The note that a model turns with a finger: drawn on the picture where the loading gem
   stood, faint, never in the way of the drag it asks for. */
.flairui-viewer-hint {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  z-index: var(--z-over);
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--motion-slow) ease;
  color: var(--color-text);
}
.flairui-viewer-hint.is-shown { opacity: var(--opacity-s); }
.flairui-viewer-hint svg { display: block; width: 56px; height: 56px; }
@media (prefers-reduced-motion: reduce) { .flairui-viewer-hint { transition: none; } }
/* Dropping a file: the whole window is the target, so the whole window says so. */
html.kb-dropping body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: calc(var(--z-scrim) - 8);
  background: var(--studio-drop-tint);
  pointer-events: none;
}
html.kb-dropping.dark-mode-active body::after { background: var(--studio-drop-tint); }

/* ── Partner embed mode (html.kb-embed; set by the worker's /embed/<id> page
   before any script runs, and by boot for ?embed=1) ───────────────────────
   An iframe on a merchant's product page shows ONLY the viewer: every piece
   of site chrome disappears, and the viewer's back/close controls go too —
   there is nowhere to go back to inside a frame. */
html.kb-embed .kuribaby-topbar,
html.kb-embed .kuribaby-footer,
html.kb-embed .kb-series-bar,
html.kb-embed #flairMain,
html.kb-embed .flairui-viewer-close,
html.kb-embed .flairui-viewer-back {
  display: none !important;
}
/* The renderer's own backdrop (its stylesheet publishes it), so the frame is one grey before the
   piece and once it is drawn (as flair.design dc437d5). */
html.kb-embed,
html.kb-embed body {
  background: var(--flair3d-backdrop);
}
/* ?bg=transparent on the embed URL (html.kb-transparent, set by the worker):
   the renderer draws with an alpha channel and the merchant's page shows
   through, so every layer between the page and the canvas has to be clear -
   the viewer, its body, the preview host and the canvas itself. */
html.kb-embed.kb-transparent,
html.kb-embed.kb-transparent body,
html.kb-embed.kb-transparent .flairui-viewer,
html.kb-embed.kb-transparent .flairui-viewer-body,
html.kb-embed.kb-transparent .flairui-viewer > *,
html.kb-embed.kb-transparent .flairui-preview-3d,
html.kb-embed.kb-transparent .flairui-preview-3d canvas {
  background: transparent !important;
}
/* What lies against the viewer window's edge (the canvas, the material column) reads the
   window's corner, which is the dialog shell's --r (0 when it fills the screen); the
   platform's viewer stylesheet (v6, read as it is) still names --modal-radius. */
.flairui-viewer .pdf-viewer-dialog { --viewer-corner: var(--r); }
/* Inside a frame the viewer is the whole page, not a window floating on one:
   the dialog fills it edge to edge, and the rounded corners, shadow, inset and
   dimmed backdrop that make a modal read as a modal all go. Anything of the
   host page still showing through — the sign-in hint, the empty grid — is
   chrome the shopper never asked for. */
html.kb-embed .flairui-viewer,
html.kb-embed .pdf-viewer-overlay {
  position: fixed !important;
  inset: 0 !important;
  padding: 0 !important;
  background: var(--flair3d-backdrop) !important;
  backdrop-filter: none !important;
}
html.kb-embed .pdf-viewer-dialog,
html.kb-embed .flairui-viewer > * {
  /* The canvas rounds its own bottom corners off this one variable — the
     renderer paints white into the pixels, so the radius has to be on the
     canvas itself. Zeroing the dialog's border-radius alone left that curve
     showing over the merchant's photograph. */
  --viewer-corner: 0px !important;
  inline-size: 100% !important;
  block-size: 100% !important;
  max-inline-size: none !important;
  max-block-size: none !important;
  margin: 0 !important;
  border: 0 !important;
  border-radius: 0 !important;
  box-shadow: none !important;
}
html.kb-embed .kuribaby-login-hint,
html.kb-embed .kuribaby-page,
html.kb-embed .studio-portfolio-section {
  display: none !important;
}
/* The solid copy glyph is drawn on a 945-unit box and inks 90% of it, where
   the menu baseline is 75%, so at 24px it reads oversized. Measured, the size
   that lands its ink on the same 18 units is 20px — the 17px this carried
   before was tuned by eye and overshot, leaving it 15% small instead. */
.kb-viewer-embed .action-icon svg {
  width: 20px !important;
  height: 20px !important;
}

/* ── Assign to product (2026-09-04) ───────────────────────────────────────
   The panel that points a partner's product at this model. Its own overlay
   rather than the synced modal (the sheet helper expects a modal that
   already exists in the DOM), drawn by the dialog shell (.ui-scrim around
   .ui-dialog). Here only where it stands: level with the viewer it is
   opened from. */
.kb-assign { z-index: var(--z-viewer); }
.kb-assign-shop {
  margin: var(--space-m) 0 var(--space-s);
  font-size: var(--font-2xs);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--color-text-secondary);   /* the library's quiet grey, not a fade (follows flair.design adaacaf) */
}
.kb-assign-note { padding: var(--space-s) 0; font-size: var(--font-s); line-height: var(--leading); color: var(--color-text-secondary); }
.kb-assign-row {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-m);
  padding: var(--space-m) var(--space-m);
  margin-bottom: var(--space-s);
  border: var(--hairline) solid var(--color-border);
  border-radius: var(--radius-m);
  background: transparent;
  color: inherit;
  font-size: var(--font-m);
  text-align: left;
  cursor: pointer;
}
.kb-assign-row:hover { background: var(--color-bg-hover); }
.kb-assign-row[disabled] { opacity: var(--dim); cursor: default; }
.kb-assign-row.is-on { border-color: currentColor; }
.kb-assign-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.kb-assign-state { flex: 0 0 auto; font-size: var(--font-xs); color: var(--color-text-secondary); }
.kb-assign-row.is-on .kb-assign-state { color: inherit; font-weight: var(--weight-medium); }
.kb-assign-input { margin-block: var(--space-xs) var(--space-s); }   /* the field shell (ui-field) draws it */
/* Adding a store, and the two steps that prove it is yours: the button shell's
   button across the dialog, one step apart. */
.kb-assign-add { margin-top: var(--space-s); }
/* An embed must never show this: it is the owner's tool, and the frame has no
   owner in it. */
html.kb-embed .kb-assign,
html.kb-embed .kb-viewer-assign { display: none !important; }

/* ── Select: the tick on a card, the selected card, the floating bar ── */
/* The tick (the shells' .ui-badge.is-tick.is-on-image) rides the thumb, the positioned, clipped box,
   at every card size; it exists only while selecting. Its ring lies outside the badge, so the corner
   gap is measured to the ring's outer edge. */
#studioPortfolioGrid .studio-portfolio-thumb > .flairui-select-tick {
  position: absolute;
  display: flex;
  top: calc(var(--space-s) - var(--chosen-ring));
  left: calc(var(--space-s) - var(--chosen-ring));
  z-index: 3;
  pointer-events: none;
}
/* The frozen preview sheet's ".is-app-icon svg" sizes every svg in an app-icon tile (Rhino, Acrobat,
   Illustrator, Photoshop) to the whole box, !important, and the tick's mark is an svg in that tile:
   Safari then widens the tick around it into a bar. The mark keeps the shells' own size there too. */
.is-app-icon .ui-badge.is-tick svg {
  width: var(--font-xs) !important;
  height: var(--font-xs) !important;
}
#studioPortfolioGrid .studio-portfolio-card.is-selected .studio-portfolio-thumb {
  box-shadow: 0 0 0 var(--chosen-ring) var(--color-text);
}
/* The last row clears the bar and the footer; both heights are measured by kuribaby-boot.js. */
#studioPortfolioGrid.kb-selecting {
  padding-bottom: calc(var(--kb-selbar-h, 48px) + 28px);
}
/* Above the page, below the menus, the viewer and every dialog. */
.kb-select-bar {
  position: fixed;
  left: 50%;
  bottom: calc(var(--kb-footer-h, 41px) + 14px + env(safe-area-inset-bottom, 0px));
  z-index: var(--z-scrim);
  display: flex;
  align-items: center;
  gap: var(--space-s);
  box-sizing: border-box;
  max-width: min(92vw, 600px);
  padding: var(--space-s) var(--space-s) var(--space-s) var(--space-l);
  border-radius: var(--radius-full);
  background: var(--color-bg-panel);
  -webkit-backdrop-filter: blur(16px) saturate(1.6);
  backdrop-filter: blur(16px) saturate(1.6);
  box-shadow: var(--menu-shadow), 0 0 0 var(--hairline) var(--color-border);
  color: var(--color-text);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translateX(-50%) translateY(10px);
  transition: opacity var(--motion) ease, transform var(--motion) cubic-bezier(0.2, 0, 0, 1), visibility var(--motion);
}
.kb-select-bar.is-open {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transform: translateX(-50%) translateY(0);
}
.dark-mode-active .kb-select-bar {
  background: var(--color-bg-panel);
  color: var(--color-text);
  box-shadow: var(--menu-shadow), 0 0 0 var(--hairline) var(--color-border);
}
.kb-select-count {
  font-size: var(--font-s);
  font-weight: var(--weight-medium);
  line-height: 1.2;
  white-space: nowrap;
  flex-shrink: 0;
  margin-right: var(--space-2xs);
  font-variant-numeric: tabular-nums;
}
/* The bar's actions, its "more" and its close are the shared button shell (one
   height, so one corner, for the row); the bar only keeps them from being
   squeezed. It never wraps: where one line does not fit, kuribaby-boot.js
   folds actions into the "more" menu one by one, measured. */
.kb-select-act,
.kb-select-more,
.kb-select-close {
  flex-shrink: 0;
}
/* On a phone the three choosing dialogs (how files go up, which collection,
   delete or not) come up from the bottom edge, as tall as what they hold.
   The sheet helper's .flairui-sheet pins a panel to the bottom, but the
   dialog shell, loaded later, makes it position: relative and takes the
   pinning away; this puts it back, as flair.design does. How the sheet looks
   (corner, colour, shadow, the bottom inset) is the shells' .ui-sheet-on-phone.
   The scrim dims the page; the backdrop is lit so a tap outside closes. */
@media (max-width: 768px), (max-height: 500px) {
  .project-modal .project-modal-content.kb-sheet {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    top: auto;
    width: auto;
    max-width: none;
    height: auto;
    max-height: min(80dvh, 80svh);
  }
  .project-modal:has(> .kb-sheet) > .project-settings-sheet-backdrop {
    display: block;
    position: absolute;
    inset: 0;
  }
  /* The 3D viewer's layer rail is a sheet here (.ui-sheet-on-phone). The platform's
     stylesheet (v6 kb-file-preview.css, read as it is) draws that sheet itself, a top line
     and its own corner, at a weight the shells cannot outrank; those give way to the
     shells' corner, and the top fade starts from the sheet's own fill. */
  .flairui-viewer.has-rail .flairui-viewer-rail.ui-sheet-on-phone:not(.is-thumbs) {
    border-top: 0;
    border-radius: var(--r) var(--r) 0 0;
  }
  .flairui-viewer-rail.ui-sheet-on-phone:not(.is-thumbs)::before {
    background: linear-gradient(to bottom, var(--color-bg-dialog) 60%, transparent);
    border-radius: inherit;
  }
}
@media (max-width: 640px) {
  .kb-select-bar {
    left: 12px;
    right: 12px;
    max-width: none;
    transform: translateY(10px);
    padding: var(--space-s) var(--space-s) var(--space-s) var(--space-l);
    justify-content: space-between;
  }
  /* The count on the left, the buttons together on the right. */
  .kb-select-count { margin-right: auto; }
  .kb-select-bar.is-open { transform: translateY(0); }
}
/* A press-and-hold must never raise the system menu on a card. */
#studioPortfolioGrid .studio-portfolio-card,
#studioPortfolioGrid .studio-portfolio-card * {
  -webkit-touch-callout: none;
  -webkit-user-drag: none;
}
html.kb-embed .kb-select-bar { display: none !important; }
