/**
 * Navbar - panels (deferred tier).
 *
 * The morphing dropdown card, the products menu, the account menu and the mobile
 * drawer, plus every keyframe. Loaded deferred; the shell in base_navbar.css is
 * what blocks render.
 *
 * COLOUR RULE (same as base_navbar.css): no colour literals. Everything derives
 * from --primary-blue via the --brand* / --nb-* variables in base_style.css.
 *
 * MOTION RULE: every transition and animation sits inside
 * `@media (prefers-reduced-motion: no-preference)`. Layout must be correct
 * without them - reduced-motion users get the same navbar, just instantly.
 */

/* ------------------------------------------------------------- keyframes */

@keyframes nbFadeIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes nbFadeOut {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
}

/*
 * Panels bloom open from their own centre rather than dropping in from above -
 * a soft scale-up plus fade, like a projector coming up, so nothing appears to
 * slide. Paired with `transform-origin: center` on the panel.
 */
@keyframes nbMegaIn {
  from {
    opacity: 0;
    transform: scale(0.94);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

/*
 * The mirror of nbMegaIn, so a panel leaves the way it arrived instead of
 * blinking out. Used when switching directly between two right-hand panels
 * (products menu <-> notifications): the outgoing one plays this while the
 * incoming one plays nbMegaIn, which reads as a cross-dissolve - the closest
 * equivalent to the way the shared morph card tweens between two bar menus.
 */
@keyframes nbMegaOut {
  from {
    opacity: 1;
    transform: scale(1);
  }
  to {
    opacity: 0;
    transform: scale(0.96);
  }
}

/* View-to-view swap inside a panel: a pure cross-fade with the faintest
   scale, so switching sections reads as a dissolve rather than a slide. */
@keyframes nbDdIn {
  from {
    opacity: 0;
    transform: scale(0.985);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

@keyframes nbDrawerIn {
  from {
    transform: translateX(100%);
  }
  to {
    transform: translateX(0);
  }
}

@keyframes nbDrawerOut {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(100%);
  }
}

/* ------------------------------------------------------------ morph card */

/*
 * One card, two panels. base_navbar.js measures the active panel and writes
 * --nb-morph-w/h/x; the card animates between those values so switching triggers
 * morphs instead of closing and reopening.
 *
 * Sizes live here rather than in JS - the script measures what these rules
 * render, so panel sizing stays a styling decision.
 */
.nb-morph {
  font-family: var(--nb-font, inherit);
  position: absolute;
  top: calc(100% + 14px);
  left: 0;
  z-index: 90;
  width: var(--nb-morph-w, 800px);
  height: var(--nb-morph-h, 0px);
  transform: translateX(var(--nb-morph-x, 0px));
  transform-origin: top left;
  background-color: var(--nb-panel);
  background-image: var(--nb-panel-bloom);
  border: 1px solid var(--border-color);
  border-radius: var(--nb-radius-panel);
  box-shadow: 0 24px 60px -12px var(--nb-shadow);
  overflow: hidden;
}

.nb-morph[hidden] {
  display: none;
}

.nb-morph__panel {
  position: absolute;
  top: 0;
  left: 0;
  opacity: 0;
  pointer-events: none;
}

/*
 * Content-sized: a single-column mega panel should not reserve three columns of
 * width. base_navbar.js measures the rendered width rather than reading a fixed
 * value, so these bounds are the only sizing knobs.
 */
/* Wide enough for a full tool directory of six columns. The panel is still
   content-sized, so a two-column menu stays narrow. */
.nb-morph__panel--mega {
  width: max-content;
  max-width: min(1240px, 94vw);
  padding: 22px;
}

.nb-morph__panel--more {
  width: max-content;
  min-width: 220px;
  max-width: min(320px, 92vw);
  padding: 8px;
}

.nb-morph__panel.is-active {
  opacity: 1;
  pointer-events: auto;
  transform: none;
}

/* One track per configured section, each sized to its content within bounds -
   so one column stays narrow instead of stretching across three-column width. */
/*
 * One track per column, content-sized within bounds - a two-column menu stays
 * narrow. The 175px floor is what lets six columns (a full tool directory) fit
 * inside the panel's max-width; the card clips its overflow, so columns that do
 * not fit are not scrolled to, they are simply lost.
 */
.nb-mega__cols {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(175px, max-content);
  gap: 8px 14px;
}

/* Below the widest desktop the single row would overflow, so let the columns
   wrap into a block instead of squeezing each one below legibility. */
@media (max-width: 1240px) {
  .nb-mega__cols {
    grid-auto-flow: row;
    grid-template-columns: repeat(3, minmax(180px, 1fr));
  }
}

/* ---------------------------------------------------------------- columns */

/* Space between stacked blocks within one column. */
.nb-col__section + .nb-col__section {
  margin-top: 20px;
}

/* Mega-menu column heading: plain bold in the page font, matching the footer
   column headings. The tracked ALL-CAPS grey eyebrow read as a system label
   rather than a section title. */
.nb-col__title {
  font-size: 14px;
  font-weight: 700;
  letter-spacing: -0.01em;
  text-transform: none;
  color: var(--heading-color);
  padding: 4px 12px 10px;
  text-align: center;
}

.nb-rich {
  /* Skeleton styles bare `a`; this anchor's text lives in child spans, so
     without a colour of its own it inherits Skeleton's link blue. */
  color: inherit;
  display: flex;
  gap: 12px;
  align-items: flex-start;
  padding: 9px 12px;
  border-radius: 14px;
  text-decoration: none;
}

.nb-rich:not(:has(.nb-rich__desc)) {
  align-items: center;
}

.nb-rich:hover {
  color: inherit;
  background: var(--brand-softer);
}

.nb-rich__tile {
  flex-shrink: 0;
  width: 38px;
  height: 38px;
  border-radius: 11px;
  background: var(--brand-soft);
  display: grid;
  place-items: center;
  color: var(--brand);
  font-size: 21px;
}

@media (prefers-reduced-motion: no-preference) {
  .nb-rich {
    transition:
      background 0.2s ease,
      transform 0.2s ease;
  }

  .nb-rich:hover {
    transform: translateX(3px);
  }

  .nb-rich__tile {
    transition:
      background 0.22s cubic-bezier(0.34, 1.56, 0.64, 1),
      color 0.22s ease,
      transform 0.22s cubic-bezier(0.34, 1.56, 0.64, 1);
  }

  /* Clean hover: a subtle pop only. The tile's tint/icon colour stay exactly
     as they are at rest - no colour inversion on hover. */
  .nb-rich:hover .nb-rich__tile {
    transform: scale(1.04);
  }

  .nb-rich--disabled,
  .nb-rich--disabled:hover {
    transform: none;
  }

  .nb-rich--disabled:hover .nb-rich__tile {
    transform: none;
  }
}

.nb-rich__text {
  min-width: 0;
}

.nb-rich__title {
  display: block;
  font-weight: 700;
  font-size: 14.5px;
  color: var(--text-primary);
}

/* Status pill beside a title, e.g. "Coming soon". */
.nb-rich__badge {
  display: inline-block;
  margin-left: 7px;
  padding: 2px 7px;
  border-radius: 20px;
  background: var(--brand-soft);
  color: var(--brand);
  font-size: 9.5px;
  font-weight: 800;
  /* px, not em: 9.5 * 0.05 = 0.475 */
  letter-spacing: 0.5px;
  text-transform: uppercase;
  vertical-align: middle;
}

/* Announced but not reachable: readable, obviously inert, no hover affordance. */
.nb-rich--disabled {
  cursor: default;
}

.nb-rich--disabled:hover {
  background: none;
}

.nb-rich--disabled .nb-rich__title,
.nb-rich--disabled .nb-rich__desc {
  color: var(--muted-text);
}

.nb-rich--disabled .nb-rich__tile {
  background: color-mix(in oklch, var(--muted-text) 12%, transparent);
  color: var(--muted-text);
}

.nb-rich__desc {
  display: block;
  font-size: 12.5px;
  color: var(--muted-text);
  margin-top: 2px;
  line-height: 1.35;
}

.nb-row--more,
.nb-row--menu,
.nb-row--rail,
.nb-row--feedback {
  display: flex;
  width: 100%;
  text-align: left;
}

/* Rail rows are their own size class: smaller and tighter than a top-level nav
   row, with muted icons so the only accent in the column is the active row. */
.nb-row--rail {
  gap: 11px;
  padding: 10px 8px;
  /* Own defaults rather than --nb-radius-row / --nb-row-size: these are the
     design's pixel-exact rail values and must not drift when a project retunes
     the generic row tokens. A project that wants the rail to follow its own
     shape sets these two. */
  border-radius: var(--nb-radius-rail, 10px);
  font-weight: var(--nb-row-weight);
  font-size: var(--nb-rail-size, 13.5px);
  font-family: var(--nb-row-font);
  color: var(--text-primary);
  white-space: normal;
  overflow-wrap: break-word;
  line-height: 1.25;
}

/*
 * Rail icons stay a clean, neutral glyph at rest - the tinted tile only
 * shows up on hover (below), so the resting list doesn't read as "every row
 * is highlighted at once."
 */
/* `:not(.fi)` matters: the flag IS a lead-idle element, and giving it a tile
   background would overwrite the flag image that .fi paints via background-image. */
.nb-row--rail .nb-row__icon,
.nb-row--rail .nb-row__lead-idle:not(.fi) {
  width: 28px;
  height: 28px;
  border-radius: 8px;
  background: transparent;
  color: var(--muted-text);
  font-size: 14px;
  display: grid;
  place-items: center;
  flex-shrink: 0;
  transition:
    background 0.22s ease,
    color 0.22s ease;
}

.nb-row--rail:hover .nb-row__icon,
.nb-row--rail:hover .nb-row__lead-idle:not(.fi) {
  background: var(--brand-soft);
  color: var(--brand);
}

/* The lead slot holds a tile now, so it needs the tile's footprint. */
.nb-row--rail .nb-row__lead {
  width: 28px;
  height: 28px;
}

/* The flag keeps its own shape and image - no tile, no background reset. */
.nb-row--rail .nb-row__flag.fi {
  width: 24px;
  height: 18px;
  border-radius: 4px;
}

/*
 * Every :hover rule below restates a colour on purpose. Skeleton (loaded from a
 * CDN) ships `a:hover { color: #0FA0CE }`, and navbar rows are anchors whose own
 * colour is set at rest on a class (0,1,0) - which that rule outranks at 0,1,1.
 * Without a colour here the labels turned Skeleton blue on hover. These tie it
 * at 0,1,1 and this sheet loads later, so they win.
 */
.nb-row--more:hover,
.nb-row--menu:hover,
.nb-row--rail:hover,
.nb-row--feedback:hover {
  color: var(--text-primary);
  background: var(--nb-row-hover-bg);
  transform: translateX(var(--nb-row-hover-shift));
}

.nb-row--rail .nb-row__chev {
  margin-left: auto;
}

/*
 * View-switch rows: the leading glyph swaps in place.
 *
 * Both glyphs occupy the same 21px slot so nothing shifts when they exchange -
 * the row's icon shows while its view is closed, and a back-arrow takes its
 * place once open. There is no trailing chevron by design; the arrow appearing
 * on the left IS the affordance.
 */
.nb-row__lead {
  position: relative;
  display: inline-grid;
  place-items: center;
  width: 21px;
  height: 21px;
  flex-shrink: 0;
}

.nb-row__lead > * {
  grid-area: 1 / 1;
  /* Buttons inherit Skeleton's line-height:38px, which made the arrow a 38px-tall
     box inside this 21px cell and threw its optical alignment off. */
  line-height: 1;
}

.nb-row__lead-open {
  opacity: 0;
  /* The back-arrow is the one brand-coloured glyph in the rail. Deliberately not
     given .nb-row__icon, whose rail rule would mute it.
     The design says 19px, but that sizes Material Symbols' hairline chevron;
     Font Awesome's is much heavier, so matching the number would overpower the
     row. 14px puts it in scale with the 15px flag it replaces. */
  font-size: 14px;
  color: var(--brand);
}

.nb-row--view.is-current .nb-row__lead-idle {
  opacity: 0;
}

.nb-row--view.is-current .nb-row__lead-open {
  opacity: 1;
}

/*
 * Active row: only the text colour changes.
 *
 * No background is declared on purpose. A fill would be wrong - in the design
 * the sole background a rail row ever gets is the hover tint - and declaring
 * `transparent` here would be worse still, because this rule sits after
 * `.nb-row--rail:hover` at equal specificity and would cancel the hover.
 */
/*
 * Plain rail rows (Dashboard, About us, Contact) take the brand on hover too.
 * The expandable ones appeared to do this already, but that was `.is-current`:
 * hovering one opens its view, and the open row is brand-coloured. The plain
 * rows have no such state, so they stayed dark and the column looked
 * inconsistent under the pointer.
 *
 * Ties the shared row-hover rule above at 0,1,1 and wins on order.
 */
.nb-row--rail:hover {
  color: var(--brand);
}

.nb-row--view.is-current {
  color: var(--brand);
}

.nb-row--danger {
  color: var(--danger);
}

.nb-row--danger .nb-row__icon {
  color: var(--danger);
}

.nb-row--danger:hover {
  color: var(--danger);
  background: color-mix(in oklch, var(--danger) 10%, transparent);
}

/* ------------------------------------------------------------- big menu */

.nb-big-wrap {
  position: relative;
}

/*
 * Sized by content, not fixed.
 *
 * `max-content` lets the panel be exactly as wide as it needs: a rail-only menu
 * is narrow, a menu with two columns of products is wide. The max-width keeps it
 * from overflowing narrow viewports. There is deliberately no min-height - an
 * empty panel should be short, not a tall blank rectangle.
 */
.nb-big {
  font-family: var(--nb-font, inherit);
  position: absolute;
  top: calc(100% + 12px);
  right: 0;
  z-index: 95;
  width: max-content;
  max-width: min(1020px, 94vw);
  display: flex;
  background-color: var(--nb-panel);
  background-image: var(--nb-panel-bloom);
  border: 1px solid var(--border-color);
  border-radius: var(--nb-radius-panel);
  box-shadow: 0 24px 60px -12px var(--nb-shadow);
  padding: 22px;
  transform-origin: center;
  /* Overlay views can extend above this box - do not clip them. */
  overflow: visible;
}

/*
 * When the body has no main-view content, collapse it entirely while the main
 * view is showing - otherwise the panel renders a tall empty column next to the
 * rail. The rail's divider and inset go with it, so the menu shrinks to just the
 * rail and expands again the moment a language/feedback view opens.
 */
.nb-big[data-nb-view='main'] .nb-big__body--no-main {
  display: none;
}

.nb-big__body--no-main ~ .nb-big__rail {
  padding-left: 0;
  border-left: none;
}

/* With no body, the rail is the whole menu: drop its divider and inset. */
.nb-big--rail-only .nb-big__rail {
  width: max-content;
  min-width: 190px;
  padding-left: 0;
  border-left: none;
}

.nb-big[hidden] {
  display: none;
}

/*
 * Positioning context for the overlay views, which anchor themselves to the
 * bottom of this pane. The main view flows from the top and uses the full
 * height - bottom-aligning here would leave a dead band above it.
 */
.nb-big__body {
  flex: 1;
  min-width: 0;
  padding-right: 22px;
  position: relative;
}

.nb-big__body--no-main {
  width: 0;
  min-width: 0;
  padding-right: 0;
}

/*
 * The main pane is always rendered. A swapped view (language, blog, feedback)
 * floats ABOVE it as its own raised card while the main content stays visible,
 * blurred and dimmed behind - the products menu never goes blank, it gets
 * layered. The overlay is anchored to the bottom-right of the pane and may grow
 * upward past the panel's own top edge (the 18-language grid does), which is why
 * .nb-big must not clip its overflow.
 */
.nb-big__view[data-nb-view-name='main'] {
  display: block;
}

.nb-big__view:not([data-nb-view-name='main']) {
  display: none;
  position: absolute;
  right: 0;
  /* Vertically aligned to the rail row that opened it - base_navbar.js writes
     the exact `top` when the view opens, clamped to the viewport. Anchoring to
     the pane's bottom instead left a short card (two rows, say) sitting far
     below the row you were pointing at, so reaching it meant cutting diagonally
     across the gap and out of the menu, which closed it. */
  top: 0;
  z-index: 2;
  /* Sized to its own content, not stretched across the pane, so the main
     columns stay partly visible (blurred) beside the card. */
  width: max-content;
  min-width: 300px;
  max-width: min(620px, 60vw);
  padding: 16px;
  background-color: var(--nb-panel);
  background-image: var(--nb-panel-bloom);
  border: 1px solid var(--border-color);
  border-radius: var(--nb-radius-panel);
  box-shadow: 0 24px 60px -12px var(--nb-shadow);
}

.nb-big[data-nb-view='lang'] .nb-big__view[data-nb-view-name='lang'],
.nb-big[data-nb-view='blog'] .nb-big__view[data-nb-view-name='blog'],
.nb-big[data-nb-view='feedback'] .nb-big__view[data-nb-view-name='feedback'] {
  display: block;
}

/*
 * A menu with no main-view content (eduloo: the body is empty, the rail is the
 * whole menu) has nothing for the open view to float above, and its body pane
 * collapses to a 22px sliver of padding. Anchoring the card absolutely inside
 * that sliver put it off to the LEFT of the menu, detached from it and rising
 * past the top of the bar.
 *
 * With no backdrop to layer over, the open view is simply the panel's body: put
 * it back in flow and drop the card chrome, so the menu grows to fit it beside
 * the rail as one continuous surface. .nb-big already carries the border,
 * radius, bloom and shadow, so repeating them here would double them up.
 */
.nb-big__body--no-main .nb-big__view:not([data-nb-view-name='main']) {
  right: 14px;
  width: max-content;
  min-width: 0;
  max-width: min(620px, 60vw);
}

/* The backdrop fades with the overlay rather than snapping between states. */
.nb-big__view[data-nb-view-name='main'] {
  transition:
    filter 0.24s ease,
    opacity 0.24s ease;
}

/* What is behind the overlay: readable as context, clearly not interactive. */
.nb-big:not([data-nb-view='main']) .nb-big__view[data-nb-view-name='main'] {
  /* Dim more than blur: a heavy blur turned the columns into smears, which read
     as a rendering fault rather than as background. */
  filter: blur(1.5px);
  opacity: 0.3;
  pointer-events: none;
}

/*
 * Recent articles.
 *
 * Flows into as many columns as the available height allows: the column height
 * is capped, so the browser fills one column, then starts another rather than
 * making the panel taller and taller as posts are added.
 */
.nb-blog {
  display: flex;
  flex-flow: column wrap;
  align-content: flex-start;
  max-height: 320px;
  gap: 2px 18px;
}

.nb-blog__item {
  /* Skeleton styles bare `a`; this anchor's text lives in child spans, so
     without a colour of its own it inherits Skeleton's link blue. */
  color: inherit;
  display: flex;
  gap: 11px;
  align-items: flex-start;
  width: 290px;
  padding: 9px 11px;
  border-radius: 12px;
  text-decoration: none;
}

.nb-blog__thumb {
  flex-shrink: 0;
  width: 52px;
  height: 40px;
  border-radius: 9px;
  overflow: hidden;
  background: var(--brand-soft);
  display: grid;
  place-items: center;
  color: var(--brand);
  font-size: 15px;
}

.nb-blog__thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.nb-blog__text {
  min-width: 0;
}

.nb-blog__item:hover {
  color: inherit;
  background: var(--brand-softer);
}

.nb-blog__title {
  display: block;
  font-weight: 700;
  font-size: 13.5px;
  color: var(--text-primary);
  line-height: 1.3;
  /* Two lines maximum, so one long headline cannot dictate the row height. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.nb-blog__excerpt {
  display: block;
  font-size: 12px;
  color: var(--muted-text);
  margin-top: 2px;
  line-height: 1.35;
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

/* In the drawer the column is already narrow, so rows go full width and the
   multi-column wrap of the desktop panel does not apply. */
.nb-blog__item--drawer {
  width: 100%;
}

.nb-blog__all {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  margin-top: 12px;
  padding: 8px 11px;
  border-radius: 10px;
  font-weight: 700;
  font-size: 13px;
  color: var(--brand);
  text-decoration: none;
}

.nb-blog__all:hover {
  color: var(--brand);
  background: var(--brand-softer);
}

/* One track per group, content-sized - same model as the mega panel, so three
   groups sit side by side instead of wrapping the third under the first. */
.nb-big__cols {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(210px, max-content);
  gap: 6px 26px;
}

/* Auto-flow must be reset too: with `grid-auto-flow: column` inherited from
   .nb-big__cols, two feedback links were placed side by side rather than
   stacked, no matter what grid-template-columns said. */
.nb-big__cols--single {
  grid-auto-flow: row;
  grid-template-columns: 1fr;
}

.nb-big__empty {
  color: var(--muted-text);
  font-size: 13.5px;
  margin: 0;
}

/* Section heading inside a swapped view (replaces the old back button - view
   switching is hover-driven, so there is nothing to go back from). */
.nb-lang-grid__title {
  font-size: 11px;
  font-weight: 600;
  /* px, not em: 11 * 0.09 = 0.99 */
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--muted-text);
  padding: 0 8px 8px;
}

/* Geo suggestion row: the language matching the visitor's detected country. */
.nb-lang-suggest {
  display: flex;
  align-items: center;
  gap: 11px;
  width: 100%;
  padding: 10px 11px;
  margin-bottom: 12px;
  border: 1px solid var(--border-color);
  border-radius: 13px;
  background: var(--brand-softer);
  text-decoration: none;
}

.nb-lang-suggest:hover {
  color: var(--text-primary);
  border-color: color-mix(in oklch, var(--brand) 45%, var(--border-color));
  background: color-mix(in oklch, var(--brand) 14%, var(--nb-panel));
}

.nb-lang-suggest__flag.fi {
  width: 30px;
  height: 21px;
  border-radius: 4px;
  box-shadow: 0 0 0 1px var(--border-color);
  flex-shrink: 0;
}

.nb-lang-suggest__text {
  flex: 1;
  min-width: 0;
}

.nb-lang-suggest__label {
  display: block;
  font-size: 11.5px;
  font-weight: 700;
  color: var(--brand);
}

.nb-lang-suggest__name {
  display: block;
  font-weight: 600;
  font-size: 13.5px;
  color: var(--text-primary);
}

.nb-lang-suggest__pin {
  font-size: 14px;
  color: var(--brand);
  flex-shrink: 0;
}

.nb-big__rail {
  width: max-content;
  min-width: 186px;
  max-width: var(--nb-rail-max, min(260px, 72vw));
  flex-shrink: 0;
  padding-left: 22px;
  border-left: 1px solid var(--border-color);
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.nb-rail__sep {
  height: 1px;
  background: var(--border-color);
  /* Enough air that the rule reads as a divider between groups rather than an
     underline on the row above it - the theme control especially sat almost on
     top of the first separator. */
  margin: 16px 8px;
}

/* ------------------------------------------------------------- languages */

.nb-lang-grid {
  display: grid;
  gap: 2px 4px;
}

.nb-lang-grid[data-lang-cols='1'] {
  grid-template-columns: 1fr;
}

.nb-lang-grid[data-lang-cols='2'] {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.nb-lang-grid[data-lang-cols='3'] {
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

.nb-lang-option {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 9px;
  border-radius: 9px;
  font-weight: 600;
  font-size: 13px;
  color: var(--text-primary);
  text-decoration: none;
}

.nb-lang-option:hover {
  color: var(--text-primary);
  background: var(--brand-softer);
}

.nb-lang-option .language-name {
  flex: 1;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.nb-lang-option .fi {
  width: 20px;
  height: 14px;
  border-radius: 2px;
  box-shadow: 0 0 0 1px var(--border-color);
  flex-shrink: 0;
}

.nb-lang-check,
.lang-suggested-icon {
  font-size: 13px;
  color: var(--brand);
  flex-shrink: 0;
}

.nb-lang-option--drawer {
  font-size: 14.5px;
  padding: 11px 12px;
}

/* ------------------------------------------------------------ theme knob */

/* The two-way theme control (.nb-theme) moved to base_theme_switcher.css: it is
   rendered by the navbar rail, the mobile drawer AND the docs top bar, and the
   docs page loads no navbar assets at all, so styling it here left it unstyled
   there. It lives with the switcher whose API it calls. */

/* ----------------------------------------------------------- account menu */

.nb-user__menu {
  position: absolute;
  top: calc(100% + 10px);
  right: 0;
  z-index: 95;
  /* Sized to its content so a long username or email does not wrap mid-word
     ("localhost@localhost.co / m"), with a floor so a short one still reads as
     a panel and a ceiling so a pathological address cannot stretch the menu
     across the viewport. Past the ceiling the address wraps at any character,
     since an email has no spaces to break on. */
  width: max-content;
  min-width: 236px;
  max-width: min(340px, 92vw);
  background: var(--nb-panel);
  border: 1px solid var(--border-color);
  border-radius: 16px;
  box-shadow: 0 20px 48px -12px var(--nb-shadow);
  padding: 8px;
  transform-origin: center;
}

.nb-user__menu[hidden] {
  display: none;
}

.nb-user__head {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 10px 11px 12px;
}

.nb-user__name {
  display: block;
  font-weight: 700;
  font-size: 14px;
  color: var(--text-primary);
}

.nb-user__mail {
  overflow-wrap: anywhere;
  display: block;
  font-size: 12.5px;
  color: var(--muted-text);
  overflow-wrap: anywhere;
}

.nb-user__sep {
  display: block;
  height: 1px;
  background: var(--border-color);
  margin: 6px 4px !important;
}

/* ---------------------------------------------------------------- drawer */

.nb-drawer {
  font-family: var(--nb-font, inherit);
  position: fixed;
  inset: 0;
  z-index: 300;
}

.nb-drawer[hidden] {
  display: none;
}

.nb-drawer__scrim {
  position: absolute;
  inset: 0;
  background: var(--nb-scrim);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
}

.nb-drawer__panel {
  position: absolute;
  top: 0;
  right: 0;
  height: 100%;
  width: min(370px, 88vw);
  background: var(--nb-surface);
  box-shadow: -20px 0 60px -20px var(--nb-shadow);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.nb-drawer__head {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 16px 18px;
  border-bottom: 1px solid var(--border-color);
}

.nb-drawer__close {
  width: 40px;
  height: 40px;
  /* Same reason as .nb-icon-btn: Skeleton's button padding would otherwise set a
     62px minimum width on this square. */
  padding: 0;
  line-height: 1;
  display: grid;
  place-items: center;
  border: 1px solid var(--border-color);
  background: var(--nb-surface);
  border-radius: 11px;
  color: var(--text-primary);
  font-size: 18px;
  cursor: pointer;
  flex-shrink: 0;
}

.nb-drawer__body {
  flex: 1;
  overflow-y: auto;
  padding: 14px 18px 18px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.nb-drawer__foot {
  flex-shrink: 0;
  border-top: 1px solid var(--border-color);
  padding: 14px 18px;
}

.nb-drawer__foot .nb-btn {
  width: 100%;
}

.nb-row--drawer,
.nb-rich--drawer {
  display: flex;
  width: 100%;
  padding: 13px 12px;
  border-radius: 13px;
  font-size: 16px;
  font-weight: 700;
  text-align: left;
}

.nb-row--drawer:hover,
.nb-rich--drawer:hover {
  color: var(--text-primary);
  background: var(--brand-softer);
}

.nb-row--drawer .nb-row__icon {
  font-size: 21px;
}

.nb-drawer__user {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 14px;
  border: 1px solid var(--border-color);
  border-radius: 16px;
  margin-bottom: 4px;
}

.nb-drawer__avatar {
  width: 46px;
  height: 46px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: var(--brand-soft);
  color: var(--brand);
  font-size: 26px;
  flex-shrink: 0;
}

.nb-drawer__user-text {
  min-width: 0;
}

.nb-drawer__user-name {
  display: block;
  font-weight: 700;
  font-size: 15px;
  color: var(--text-primary);
}

.nb-drawer__user-mail {
  display: block;
  font-size: 12.5px;
  color: var(--muted-text);
  overflow-wrap: anywhere;
}

/* Divider between blocks of the drawer. Inset slightly so it reads as a group
   break rather than a full-width cut across the panel. */
.nb-drawer__sep {
  display: block;
  /*
   * A margin-free divider: a 1px line centred in a 21px-tall box, inset 12px
   * each side. The spacing is the box height, not margin, on purpose - some
   * projects ship an aggressive mobile reset (eduloo's styles.css carries
   * `:where(body) :where(...) { margin: 0 !important }`) that would collapse a
   * margin-based gap, and the navbar must not answer !important with !important.
   * A background line has no margin to zero, so it survives the reset.
   */
  height: 21px;
  /* The drawer body is a flex column; without this the empty 21px box is
     compressed toward its 0 content height when the list overflows, and the
     gap collapses. */
  flex-shrink: 0;
  background: linear-gradient(var(--border-color), var(--border-color))
    no-repeat center / calc(100% - 24px) 1px;
}

.nb-drawer__section-title,
.nb-drawer__theme-label {
  font-size: 12px;
  font-weight: 600;
  /* px, not em: 12 * 0.09 = 1.08 */
  letter-spacing: 1.1px;
  text-transform: uppercase;
  color: var(--muted-text);
  padding: 16px 12px 4px;
}

.nb-drawer__theme {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 4px 12px;
}

.nb-drawer__theme-label {
  padding: 0;
}

.nb-drawer__stores {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

/* -------------------------------------------------------------- collapse */

/* First collapse after a run of plain rows starts a new block. */
.nb-row--drawer + .nb-collapse {
  margin-top: 6px;
}

/*
 * Trigger + body used to be direct children of `.nb-drawer__body`'s flex
 * column, so its `gap` landed on both sides of the body - even collapsed to
 * 0 height, a flex gutter still counts it as an item, doubling the space
 * around every closed collapse compared to two plain rows sitting next to
 * each other. Wrapping the pair in one flex item makes a collapse consume
 * exactly one gutter, same as a row.
 */
.nb-collapse {
  display: flex;
  flex-direction: column;
}

.nb-collapse__trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  padding: 13px 12px;
  border: none;
  background: none;
  border-radius: 13px;
  font-family: inherit;
  font-weight: 700;
  font-size: 16px;
  color: var(--text-primary);
  cursor: pointer;
}

.nb-collapse__trigger:hover,
.nb-collapse__trigger[aria-expanded='true'] {
  color: var(--brand);
}

.nb-collapse__label {
  display: flex;
  align-items: center;
  gap: 12px;
  min-width: 0;
}

.nb-collapse__label .fi {
  width: 24px;
  height: 16px;
  border-radius: 3px;
  box-shadow: 0 0 0 1px var(--border-color);
  flex-shrink: 0;
}

.nb-collapse__chev {
  font-size: 15px;
  color: var(--muted-text);
  flex-shrink: 0;
}

/* height:auto is not animatable, so the open state uses a generous max-height
   upper bound rather than a measured value. */
.nb-collapse__body {
  flex-shrink: 0;
  overflow: hidden;
  max-height: 0;
  opacity: 0;
}

.nb-collapse__body.is-open {
  max-height: 900px;
  opacity: 1;
}

.nb-collapse__inner {
  /* Top padding replaces the 4px the outer flex `gap` used to add between
     the trigger and its body before the two were wrapped into one item. */
  padding: 6px 4px 8px 10px;
}

.nb-collapse__inner--lang {
  display: flex;
  flex-direction: column;
}

/* --------------------------------------------------------------- motion */

@media (prefers-reduced-motion: no-preference) {
  /*
   * The card tweens size and position between panels. On the FIRST open there is
   * nothing to morph from, so base_navbar.js adds .nb-morph--instant for one
   * frame - otherwise the card would visibly grow from zero.
   */
  .nb-morph {
    transition:
      width 0.44s cubic-bezier(0.33, 1, 0.68, 1),
      height 0.44s cubic-bezier(0.33, 1, 0.68, 1),
      transform 0.44s cubic-bezier(0.33, 1, 0.68, 1),
      opacity 0.2s ease;
    animation: nbFadeIn 0.2s ease;
  }

  .nb-morph--instant {
    transition:
      opacity 0.2s ease,
      transform 0.2s ease;
  }

  .nb-morph--closing {
    opacity: 0;
    transform: translateX(var(--nb-morph-x, 0px)) scale(0.97);
    animation: none;
  }

  .nb-morph__panel {
    transition:
      opacity 0.24s ease,
      transform 0.38s cubic-bezier(0.33, 1, 0.68, 1);
  }

  /* Panels slide in from the side they sit on, so the swap reads directionally. */
  .nb-morph__panel--mega {
    transform: translateX(-16px);
  }

  .nb-morph__panel--more {
    transform: translateX(16px);
  }

  .nb-big,
  .nb-user__menu {
    animation: nbMegaIn 0.28s cubic-bezier(0.22, 1, 0.36, 1);
  }

  /*
   * Morphing one right-hand panel into another. `--morph` suppresses the entry
   * animation (it would play underneath the tween); `--morphing` carries the
   * tween itself. base_navbar.js writes the from/to geometry inline, the same
   * split of duties .nb-big--resizing already uses.
   */
  /*
   * Doubled class on purpose (0,2,0). `.nb-notif__panel` declares its entry
   * animation LATER in this file at equal specificity, so a single class lost
   * and nbMegaIn kept running - and a running animation overrides inline
   * styles, so the from-geometry written by JS was discarded and the panel
   * started from the animation's scale(0.94) instead. That was the stutter.
   */
  .nb-panel--morph.nb-panel--morph {
    animation: none;
  }

  .nb-panel--morphing {
    transition:
      width 0.34s cubic-bezier(0.22, 1, 0.36, 1),
      height 0.34s cubic-bezier(0.22, 1, 0.36, 1),
      transform 0.34s cubic-bezier(0.22, 1, 0.36, 1);
    overflow: hidden;
  }

  /*
   * Fade the incoming panel's CONTENT in over the tween, the way the bar card
   * cross-fades its panels. The box animates its width, and fluid content
   * reflows to match - text that started laid out at the outgoing panel's width
   * re-wraps as the box narrows. `ease-in` keeps the content near-transparent
   * through the early/middle of the morph, where the reflow is worst, and only
   * resolves it once the box has essentially reached its final width. The direct
   * children are the content; the box's own surface (background, border) does
   * not fade with them.
   */
  .nb-panel--morphing > * {
    animation: nbFadeIn 0.34s ease-in both;
  }

  /* base_navbar.js holds the panel in the DOM for the length of this before
     setting `hidden`, so the exit is actually seen. */
  /*
   * Doubled selectors (0,2,0), for the same reason --morph is doubled: the
   * panels' own `animation: nbMegaIn` is declared at equal single-class
   * specificity - and for the notification panel it is declared LATER in this
   * file, so at a plain 0,1,0 the closing rule lost and the panel played its
   * ENTRY animation backwards on close. 0,2,0 wins regardless of source order,
   * and beats the --morph hold (also 0,2,0) because this rule comes after it.
   */
  .nb-big--closing.nb-big--closing,
  .nb-user__menu--closing.nb-user__menu--closing,
  .nb-notif__panel--closing.nb-notif__panel--closing {
    animation: nbMegaOut 0.2s ease forwards;
    pointer-events: none;
  }

  /*
   * Only while JS is tweening between two measured sizes. Scoped to the class so
   * the panel is not permanently transitioning width - that would also animate
   * the initial open, where it should simply appear at its natural size.
   */
  .nb-big--resizing {
    transition:
      width 0.36s cubic-bezier(0.22, 1, 0.36, 1),
      height 0.36s cubic-bezier(0.22, 1, 0.36, 1);
    overflow: hidden;
  }

  /*
   * Overlay views animate in AND out.
   *
   * Animating out needs `transition-behavior: allow-discrete` plus
   * `@starting-style`: `display` is a discrete property, so without them the
   * element would vanish the instant it flips to `display: none` and only the
   * entry would ever be visible. allow-discrete defers that flip until the
   * transition finishes; @starting-style supplies the from-state for an element
   * that was not previously rendered.
   */
  .nb-big__view:not([data-nb-view-name='main']) {
    opacity: 0;
    transform: translateY(4px) scale(0.99);
    transition:
      opacity 0.12s ease,
      transform 0.16s cubic-bezier(0.22, 1, 0.36, 1),
      display 0.16s allow-discrete;
  }

  .nb-big[data-nb-view='lang'] .nb-big__view[data-nb-view-name='lang'],
  .nb-big[data-nb-view='blog'] .nb-big__view[data-nb-view-name='blog'],
  .nb-big[data-nb-view='feedback'] .nb-big__view[data-nb-view-name='feedback'] {
    opacity: 1;
    transform: none;
  }

  @starting-style {
    .nb-big[data-nb-view='lang'] .nb-big__view[data-nb-view-name='lang'],
    .nb-big[data-nb-view='blog'] .nb-big__view[data-nb-view-name='blog'],
    .nb-big[data-nb-view='feedback']
      .nb-big__view[data-nb-view-name='feedback'] {
      opacity: 0;
      transform: translateY(4px) scale(0.99);
    }
  }

  .nb-theme__knob {
    transition:
      transform 0.42s cubic-bezier(0.22, 1, 0.36, 1),
      background 0.3s ease;
  }

  /* The icon and its back-arrow cross-fade in place rather than popping. */
  .nb-row__lead-idle,
  .nb-row__lead-open {
    transition: opacity 0.18s ease;
  }

  .nb-theme__btn,
  .nb-rich,
  .nb-lang-option,
  .nb-row--more,
  .nb-row--menu,
  .nb-row--rail,
  .nb-row--drawer,
  .nb-rich--drawer {
    transition:
      background 0.15s ease,
      color 0.2s ease,
      transform 0.22s ease;
  }

  .nb-collapse__body {
    transition:
      max-height 0.38s cubic-bezier(0.4, 0, 0.2, 1),
      opacity 0.3s ease;
  }

  .nb-collapse__chev {
    transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
  }

  .nb-collapse__trigger[aria-expanded='true'] .nb-collapse__chev {
    transform: rotate(180deg);
  }

  .nb-drawer__scrim {
    animation: nbFadeIn 0.22s ease;
  }

  .nb-drawer__panel {
    animation: nbDrawerIn 0.34s cubic-bezier(0.16, 1, 0.3, 1);
  }

  .nb-drawer--closing .nb-drawer__scrim {
    animation: nbFadeOut 0.3s ease forwards;
  }

  .nb-drawer--closing .nb-drawer__panel {
    animation: nbDrawerOut 0.32s cubic-bezier(0.4, 0, 1, 1) forwards;
  }
}

/* ---------------------------------------------------------- notifications */

/*
 * Navbar bell. Rebuilt on the nb- surfaces after the navbar rewrite; the
 * `.navbar-notif-*` class names that base_notifications.js binds to are kept on
 * the elements it queries, but none of the old .navbar / .dropdown-content
 * chrome is involved any more, so none of the specificity fights the previous
 * version needed are required here.
 */
.nb-notif {
  position: relative;
}

.nb-notif__badge {
  position: absolute;
  top: 0;
  right: 0;
  z-index: 1;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: 20px;
  border: 2px solid var(--nb-surface);
  background: var(--danger);
  color: var(--danger-ink);
  font-size: 11px;
  font-weight: 700;
  line-height: 14px;
  text-align: center;
  pointer-events: none;
}

.nb-notif__panel {
  position: absolute;
  top: calc(100% + 10px);
  right: 0;
  z-index: 95;
  width: min(360px, 92vw);
  display: flex;
  flex-direction: column;
  background: var(--nb-panel);
  border: 1px solid var(--border-color);
  border-radius: 18px;
  box-shadow: 0 24px 60px -12px var(--nb-shadow);
  overflow: hidden;
  transform-origin: center;
}

.nb-notif__panel[hidden] {
  display: none;
}

.nb-notif__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 14px 16px 10px;
}

.nb-notif__title {
  font-weight: 700;
  font-size: 15px;
  color: var(--text-primary);
}

.nb-notif__markall {
  border: none;
  background: none;
  padding: 0;
  font-family: inherit;
  font-weight: 700;
  font-size: 12.5px;
  color: var(--brand);
  cursor: pointer;
}

/*
 * Skeleton (CDN) ships `button:hover { color: #333 }` at 0,1,1, which outranks
 * the 0,1,0 colour above - the label went near-black on hover, invisible
 * against the dark panel. Restating it ties Skeleton and wins on load order.
 *
 * The affordance is a shift within the brand rather than an underline: these
 * two controls sit side by side, one a <button> and one an <a>, and projects
 * commonly kill link underlines outright (eduloo has
 * `a { text-decoration: none !important }`), so an underline would appear on
 * the button only. Mixing toward the ink darkens on light and lightens on dark,
 * so one declaration covers both themes and shifts no layout.
 */
.nb-notif__markall:hover,
.nb-notif__seeall:hover {
  color: color-mix(in oklch, var(--brand) 72%, var(--text-primary));
}

/* Header and footer stay put; only the feed scrolls. A custom thin
   scrollbar (matching .custom-dropdown-options / .base-sidebar__content /
   .command-palette__results) keeps this consistent rather than relying on
   the native OS overlay scrollbar, which flashes in on content change and
   fades out after a moment of inactivity. */
.nb-notif__list {
  flex: 0 1 auto;
  min-height: 0;
  max-height: 336px;
  overflow-y: auto;
  overflow-x: hidden;
  margin: 0;
  padding: 0;
  list-style: none;
  scrollbar-width: thin;
  scrollbar-gutter: stable;
}
.nb-notif__list::-webkit-scrollbar {
  width: 8px;
}
.nb-notif__list::-webkit-scrollbar-track {
  background: transparent;
}
.nb-notif__list::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, var(--text-color) 18%, transparent);
  border-radius: 4px;
  border: 2px solid transparent;
  background-clip: content-box;
}
.nb-notif__list::-webkit-scrollbar-thumb:hover {
  background: color-mix(in srgb, var(--text-color) 28%, transparent);
  background-clip: content-box;
}

.nb-notif__empty {
  margin: 0;
  padding: 18px 16px 22px;
  text-align: center;
  font-size: 13px;
  color: var(--muted-text);
}

.nb-notif__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 10px 16px 13px;
  border-top: 1px solid var(--border-color);
}

.nb-notif__filter {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: 12.5px;
  color: var(--muted-text);
  cursor: pointer;
}

/*
 * The tick uses the project's brand. base_elements.css's custom checkbox
 * defaults to --primary-color, and --primary-color is deliberately NOT the
 * brand - it is purple on light and teal on dark - so the checkbox came out a
 * colour that appears nowhere else in the navbar.
 */
.nb-notif__filter input[type='checkbox'] {
  border-color: var(--border-color);
}

.nb-notif__filter input[type='checkbox']:checked {
  background: var(--brand);
  border-color: var(--brand);
}

.nb-notif__seeall {
  font-weight: 700;
  font-size: 12.5px;
  color: var(--brand);
  text-decoration: none;
}

/* Rows are built by base_notifications.js, so these class names are its
   contract and must not be renamed. */
.navbar-notif-item {
  display: flex;
  gap: 11px;
  align-items: flex-start;
  padding: 11px 16px;
  border-top: 1px solid var(--border-color);
  cursor: pointer;
}

.navbar-notif-item:hover {
  background: var(--brand-softer);
}

.navbar-notif-item.is-unread {
  background: color-mix(in oklch, var(--brand) 7%, transparent);
}

.navbar-notif-icon {
  flex-shrink: 0;
  width: 32px;
  height: 32px;
  border-radius: 10px;
  display: grid;
  place-items: center;
  background: var(--brand-soft);
  color: var(--brand);
  font-size: 14px;
}

.navbar-notif-icon--success {
  background: color-mix(in oklch, var(--success) 16%, transparent);
  color: var(--success);
}

.navbar-notif-icon--muted {
  background: color-mix(in oklch, var(--muted-text) 14%, transparent);
  color: var(--muted-text);
}

.navbar-notif-text {
  min-width: 0;
  font-size: 13.5px;
  font-weight: 600;
  color: var(--text-primary);
  line-height: 1.35;
}

.navbar-notif-body {
  display: block;
  margin-top: 2px;
  font-weight: 500;
  font-size: 12.5px;
  color: var(--muted-text);
}

.navbar-notif-time {
  display: block;
  margin-top: 3px;
  font-size: 11.5px;
  color: var(--muted-text);
}

.navbar-notif-loading {
  padding: 18px 16px;
  text-align: center;
  font-size: 13px;
  color: var(--muted-text);
  list-style: none;
}

@media (prefers-reduced-motion: no-preference) {
  .nb-notif__panel {
    animation: nbMegaIn 0.28s cubic-bezier(0.22, 1, 0.36, 1);
  }

  .navbar-notif-item {
    transition: background 0.15s ease;
  }
}
