/* ------------------------------
   Workspace chrome
   ------------------------------
   The top bar, the account menu that hangs off it, and the settings dialog
   that menu opens — lifted out of app.css so that /docs and the Explore
   pages can show the same header to a signed-in visitor.

   They could not simply load app.css: that sheet also lays out the app
   shell (`body { overflow: hidden }`, `#app-root { height: 100vh }`,
   `main { display: flex }`), which would leave a document page unable to
   scroll. Same reasoning that split out nav.css, footer.css and
   project-grid.css before it.

   Loaded from the head partial, so every page has it. The rules match
   nothing on a page that does not render the "topbar" partial.

   Markup: templates/partials/topbar.html. Behaviour: public/js/topbar.mjs.
   -------------------------------------------------------------- */

/* ------------------------------------------------------- icons, unupgraded --- */

/* An icon that has not upgraded yet has NO BOX AT ALL.
   <ui-icon> is an unknown element until its module runs, which makes it
   display: inline with zero width — so every label beside one sits 16px to
   the left of where it belongs, and the whole sidebar jumps sideways the
   moment the module lands. On a warm localhost that gap is 25ms; over a
   real connection it is however long the module takes to arrive, and it is
   the blink you see on the sidebar.

   The element sizes itself from its `size` attribute at upgrade (see
   ui/icon/index.mjs); these rules say the same thing in CSS, so the space is
   already there when the server's HTML paints and nothing moves afterwards.
   1.5rem is the element's own default, for an icon with no size attribute.

   TWINNED with the sizes actually used in templates/ — TestIconSizesReserved
   fails if a template starts using one this file does not know about. */
ui-icon:not(:defined) {
  display: inline-flex;
  inline-size: 1.5rem;
  block-size: 1.5rem;
  /* Never shrink: in the flex rows these sit in, a zero-width item is
     exactly what the reservation is trying to prevent. */
  flex: none;
}

ui-icon:not(:defined)[size="12px"] { inline-size: 12px; block-size: 12px; }
ui-icon:not(:defined)[size="14px"] { inline-size: 14px; block-size: 14px; }
ui-icon:not(:defined)[size="16px"] { inline-size: 16px; block-size: 16px; }
ui-icon:not(:defined)[size="18px"] { inline-size: 18px; block-size: 18px; }
ui-icon:not(:defined)[size="30px"] { inline-size: 30px; block-size: 30px; }

/* ---------------------------------------------------------- which header --- */

/* [data-auth] is set on <html> before first paint by the inline script in
   the head partial, so neither header is ever briefly visible in the wrong
   state. Both are in the DOM on a public page; these two rules pick one.

   Scoped to .topbar--public rather than .topbar so that app.html's own
   header can never be hidden — it renders the plain "topbar" partial, and
   if storage were blocked there [data-auth] would be absent and the first
   rule would otherwise take the workspace's header away entirely. */
:root:not([data-auth="in"]) .topbar--public {
  display: none;
}

/* Only where there is actually a replacement on the page. /terms, /privacy
   and the homepage render the marketing header without a topbar beside it,
   and an unconditional rule here left a signed-in visitor to those pages
   with no header at all. :has() keys the hiding to the thing doing the
   replacing rather than to the attribute alone, so adding the topbar to
   another page is still a one-line change to that template. */
:root[data-auth="in"] body:has(.topbar--public) .site-header {
  display: none;
}

/* -------------------------------------------------------------- topbar --- */

.topbar {
  display: flex;
  align-items: center;
  gap: var(--pf-space-4);
  padding-inline: 0 var(--pf-space-4);
  border-bottom: 1px solid var(--pf-color-border);
  background: var(--pf-color-bg);
  min-height: 52px;
  /* The account menu hangs over the first row of cards. Those cards carry a
     z-index:1 overlay, and once signed in this bar is a stacking context
     (view-transition-name below) so the panel's own z-index cannot escape
     it. Without a z-index here the overlay paints on top of the open menu. */
  position: relative;
  z-index: 10;
}

.topbar .brand {
  display: flex;
  align-items: center;
  gap: var(--pf-space-2);
  line-height: 1;
  font-weight: var(--pf-weight-medium);
  font-size: var(--pf-text-base);
  letter-spacing: -0.01em;
  color: var(--pf-color-ink);
  width: 220px;
  flex-shrink: 0;
  border-right: 1px solid var(--pf-color-border);
  padding-left: var(--pf-space-4);
  padding-block: var(--pf-space-2);
  background: var(--pf-color-surface-sunken);
  /* Fills the bar's height, and NOT via height: 100% — that needs an
     ancestor with a definite height, which only /app has. There app.css
     sets html/body to height:100% and #app-root to 100dvh; on /docs and
     Explore the tallest thing above this is body's min-height: 100dvh, and
     a min-height never makes a height definite, so the percentage resolved
     to auto and the block collapsed to its text (34px in a 52px bar).

     align-self answers the same question without asking about ancestors: a
     flex item stretched on the cross axis fills the line whatever the
     container's height turns out to be. It has to be set per item because
     .topbar centres everything else. */
  align-self: stretch;
}

.topbar .brand__mark {
  height: 1.15em;
  width: auto;
  flex: none;
  fill: currentColor;
}

.topbar .brand:hover {
  color: var(--pf-color-ink);
}

/* Recedes next to the wordmark so "Platform" stays the subject. Same
   letter-spacing as the demo's Admin pill — it is the same kind of mark.
   Hidden for members via [data-admin-only]; the 220px column still lines
   up with the sidebar either way. */
.topbar .brand__admin {
  flex: none;
  font-size: var(--pf-text-xs);
  font-weight: var(--pf-weight-medium);
  letter-spacing: 0.06em;
  opacity: 0.55;
}

.topbar .topbar-nav {
  display: flex;
  align-items: center;
  gap: var(--pf-space-4);
  padding-left: var(--pf-space-2);
}

/* The one link the bar still carries. Laid out as a row so the arrow sits
   on the text's baseline-ish centre rather than above it. */
.topbar .topbar-back {
  display: flex;
  align-items: center;
  gap: var(--pf-space-1);
}

/* Sized down from the 24px the icon ships at — that is a touch target, and
   this is a glyph beside 14px text. fill follows the link's own color, so
   it dims and brightens with the hover state instead of staying put. */
.topbar .topbar-back svg {
  width: 14px;
  height: 14px;
  flex: none;
  fill: currentColor;
}

.topbar .navlink {
  font-size: var(--pf-text-sm);
  color: var(--pf-color-muted);
}

.topbar .navlink:hover {
  color: var(--pf-color-ink);
}

/* Where you are. Styled off aria-current rather than a class beside it, so
   the thing a screen reader announces and the thing a sighted visitor sees
   cannot drift apart — there is only one source for both.

   Ink against the muted the other two keep, and nothing else — the same
   weight and position, so the row does not shift as you move between
   sections. (An underline in --pf-color-primary was tried here and dropped.) */
.topbar .navlink[aria-current="page"] {
  color: var(--pf-color-ink);
}

/* Nothing marks the current link in the narrow-screen panel: those links are
   already --pf-color-ink at display size, so the bar's contrast step has
   nothing left to say there. */

.topbar-spacer {
  flex: 1;
}

/* New project lives in the bar, immediately left of Copy MCP token, and
   only on the Projects collection. Default hidden: this markup is shared
   with /docs and Explore. Specificity has to beat `.topbar .btn { display:
   inline-flex }` above, or the button would show on every signed-in page. */
.topbar > .topbar-new-project.btn {
  display: none;
}

:root[data-view="projects"] .topbar > .topbar-new-project.btn {
  display: inline-flex;
  flex-shrink: 0;
}

/* ----------------------------------------------------------- workspace --- */

/* Here rather than in app.css for exactly the reason the topbar is: the
   Explore pages show this sidebar to a signed-in visitor and cannot load
   that sheet. The token-group rules that only ever appear inside a project
   stay behind in app.css. */

/* The two-column workspace: a 220px sidebar and the view beside it. Both the
   Projects list and a single project use it, which is what makes the sidebar
   feel structural — the column is always there, and only its contents change.
   The topbar's brand block is the same width, so the two line up. */
.project-page,
.explore-project-page,
.workspace {
  display: flex;
  height: 100%;
  min-height: 0;
}

/* The Projects view's own sidebar. Shares .token-sidebar for the column
   itself — width, sunken background, the border down its right edge — and
   adds only what a list of destinations needs. */
.app-sidebar__nav {
  display: flex;
  flex-direction: column;
}

.app-sidebar__link {
  display: flex;
  align-items: center;
  gap: var(--pf-space-2);
  padding: 9px var(--pf-space-5);
  border-left: 4px solid transparent;
  font-size: var(--pf-text-sm);
  color: var(--pf-color-muted);
}

.app-sidebar__link ui-icon {
  flex: none;
}

/* TEMPORARY: the Members directory is hidden while the roster is small
   enough that a public list of it undersells the product. Hidden, not
   removed — the page, its route and its API are all live, so /app/members
   still works for anyone who has the link, and bringing it back is deleting
   this rule. AppSidebar.ensureMembersLink() re-inserts this link on an older
   cached shell, which is the other reason the hiding is here rather than in
   the markup: one rule covers both the template's copy and the script's. */
.app-sidebar__link[data-nav="members"] {
  display: none;
}

.app-sidebar__link:hover {
  color: var(--pf-color-ink);
  background: var(--pf-color-bg);
}

/* Same treatment the token groups get for the current one — see
   .token-sidebar button.active, which this deliberately mirrors rather than
   inventing a second way to say "you are here". */
.app-sidebar__link[aria-current="page"] {
  background: var(--pf-color-surface);
  border-left-color: var(--pf-color-primary);
  color: var(--pf-color-ink);
  font-weight: var(--pf-weight-medium);
}

/* Admin tools sit after Send feedback: they are staff destinations, not
   part of the everyday workspace a member has. The wrapper is
   display:contents when shown, so the fold button is a flex child of the
   sidebar the same way Pinned is — one band, not a box inside a box. */
.app-sidebar__admin:not([hidden]) {
  display: contents;
}

/* The heading that collapses a band. Every declaration here beyond the
   layout is undoing a <button> default — width, border, background, font
   and text-align — so it reads exactly as the plain title it replaced.
   The .sidebar-title rules above still supply the type and the padding.
   Admin and Pinned share this; they are the same kind of control. */
.token-sidebar .app-sidebar__fold {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--pf-space-2);
  width: 100%;
  margin-top: var(--pf-space-4);
  padding-top: var(--pf-space-4);
  border: none;
  border-top: 1px solid var(--pf-color-border);
  background: none;
  font-family: inherit;
  text-align: left;
  cursor: pointer;
}

.token-sidebar .app-sidebar__fold ui-icon {
  flex: none;
  color: var(--pf-color-muted);
  transition: transform var(--pf-duration-fast) var(--pf-ease-out);
}

.token-sidebar .app-sidebar__fold:hover ui-icon {
  color: var(--pf-color-ink);
}

/* Points down at the list it opens, and turns to point at the heading when
   there is nothing below it — the disclosure convention, and the reason the
   icon is a chevron rather than a plus. */
.token-sidebar .app-sidebar__fold[aria-expanded="false"] ui-icon {
  transform: rotate(-90deg);
}

/* A sidebar entry that acts rather than navigates — the feedback form.
   Every declaration here is undoing a <button> default so it sits in the
   column exactly as the link above it does; the shared .app-sidebar__link
   supplies everything else, including the hover. Same approach, and the
   same reason, as .app-sidebar__fold below. */
.app-sidebar__action {
  width: 100%;
  border-right: 0;
  border-top: 0;
  border-bottom: 0;
  background: none;
  font: inherit;
  font-size: var(--pf-text-sm);
  text-align: left;
  cursor: pointer;
}

/* The rule below the shelf, before the reference link. Same treatment as
   .app-sidebar__fold above it, so the sidebar reads as three bands
   separated the same way rather than two ideas of a divider. */
.app-sidebar__nav--end {
  margin-top: var(--pf-space-4);
  padding-top: var(--pf-space-4);
  border-top: 1px solid var(--pf-color-border);
}

.app-sidebar__pins {
  display: flex;
  flex-direction: column;
}

/* Swatch, name, visibility. A row rather than inline text so the name can
   take the space that is left and the state icon keeps its own — the icons
   are flex: none, so a long name shortens instead of pushing them out. */
.app-sidebar__pin {
  display: flex;
  align-items: center;
  gap: var(--pf-space-2);
}

/* Space held open for pins that have not been drawn yet.

   The height is MEASURED rather than computed: the sidebar records what the
   shelf actually came out at last time (see rememberPins) and the inline
   script in partials/app-sidebar.html reserves exactly that before first
   paint. Deriving it from the row's padding and line box instead would be a
   third place that has to agree with the two above, and it was wrong by
   8px a row the first time I tried — near enough to look deliberate and far
   enough to still shift the column.

   Zero by default, which is the honest reading of a first visit: hold open
   only what there is reason to believe is coming. */
.app-sidebar__pins {
  min-block-size: var(--pins-reserved, 0px);
}

.app-sidebar__pin-mark,
.app-sidebar__pin-state {
  flex: none;
}

/* The one part that gives way. min-width: 0 is what actually lets it — a
   flex item will not shrink below its content without it, and the ellipsis
   would never appear. */
.app-sidebar__pin-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* Public or private, shown on hover and on keyboard focus. Hidden with
   opacity rather than display so the name's available width does not change
   when it appears — otherwise a truncated title would re-flow under the
   pointer. */
.app-sidebar__pin-state {
  opacity: 0;
  color: var(--pf-color-faint);
  transition: opacity var(--pf-duration-fast) var(--pf-ease-out);
}

.app-sidebar__pin:hover .app-sidebar__pin-state,
.app-sidebar__pin:focus-visible .app-sidebar__pin-state {
  opacity: 1;
}

.app-sidebar__empty {
  margin: 0;
  padding: 0 var(--pf-space-5);
  font-size: var(--pf-text-xs);
  color: var(--pf-color-faint);
  line-height: var(--pf-leading-body);
}

/* The grid beside the sidebar owns the scrolling now that it is no longer
   the whole pane. */
/* Explore, once the sidebar is beside it. body is a flex column whose main
   carries flex: 1 (tokens.css); that grow now belongs to the wrapper, and
   what was main becomes the flexible column inside it. min-width: 0 so a
   wide token table scrolls rather than pushing the sidebar off. /app uses
   the same shape: the sidebar is in app.html, main is the pane beside it. */
.workspace > main {
  flex: 1;
  min-width: 0;
  min-height: 0;
}

/* The wrapper takes main's place as the growing row of the page. */
body > .workspace,
#app-root > .workspace {
  flex: 1;
  min-height: 0;
}

/* The column is in the markup so a signed-in visit does not wait on JS to
   grow a sidebar. Hidden here until [data-auth] says there is an account. */
:root:not([data-auth="in"]) .workspace > .token-sidebar {
  display: none;
}

/* A project owns this column for its token groups. Hidden rather than
   removed, so returning to the list does not grow the sidebar from nothing. */
:root[data-view="project"] .workspace > app-sidebar,
.workspace:has(project-page) > app-sidebar {
  display: none;
}

/* Same names on /app and /explore, so a real navigation between them keeps
   the chrome instead of fading it out and back in. Signed-in only: the
   public copy of the topbar is display:none when logged out, and a name on
   a hidden element would still participate in the transition. */
:root[data-auth="in"] .topbar {
  view-transition-name: workspace-topbar;
}

:root[data-auth="in"] app-sidebar {
  view-transition-name: workspace-sidebar;
}

/* The body is a two-column grid in site.css — 14rem for the group nav, the
   rest for the tokens. Once the nav has been lifted into the sidebar there
   is only one child left, and it would otherwise be laid into that 14rem
   first column and read as a narrow strip.

   :has() rather than a class set from JS, so the layout is a consequence of
   where the nav actually is: signed out, nothing moves and this never
   matches. (0,2,0) beats site.css's own (0,1,0) whatever the load order.) */
.explore-project__body:not(:has(.explore-groups)) {
  grid-template-columns: minmax(0, 1fr);
}

/* A published project's groups, once they have been moved into the column.
   .explore-groups is sticky where it sits inside the page, which is wrong in
   a sidebar that scrolls with its own overflow — it would peel away from the
   top of a long list. */
.explore-sidebar .explore-groups {
  position: static;
}

/* The rows match the editor's token groups rather than keeping the inline
   look they had in the body: same column, same rows, so a system reads the
   same way whether you own it or found it.

   Four classes so this outranks `.token-sidebar button:not(...):not(...)`
   in app.css (0,3,1), which would otherwise keep `background: none` and a
   transparent left border on the current row. */
.token-sidebar.explore-sidebar button.explore-group {
  padding: 9px var(--pf-space-5);
  border-left: 4px solid transparent;
}

.token-sidebar.explore-sidebar button.explore-group:hover {
  color: var(--pf-color-ink);
  background: var(--pf-color-bg);
}

.token-sidebar.explore-sidebar button.explore-group.active,
.token-sidebar.explore-sidebar button.explore-group.active:hover {
  background: var(--pf-color-surface);
  border-left-color: var(--pf-color-primary);
  color: var(--pf-color-ink);
  font-weight: var(--pf-weight-medium);
}

/* A group row: type icon, name, count.
   The editor's sidebar and Explore's draw the same row from different markup
   (.group-list button vs .explore-group), so this is keyed on the parts
   rather than on either host — the alternative is two rules that drift.

   The name takes the free space and truncates, which is what pins the count
   to the right edge however long a group is named. Users name these freely
   now that group names are no longer slugified, so "Marketing site accent
   colors" has to degrade to an ellipsis rather than push the count off. */
.group-list__name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* Faint by default: the icon says what KIND of group this is, which is
   supporting information — the name is what you are reading down the list
   for. */
.group-list button > ui-icon,
.explore-group > ui-icon {
  flex: none;
  color: var(--pf-color-faint);
  transition: color var(--pf-duration-fast) var(--pf-ease-out);
}

/* On the current row, and under the pointer, the icon takes the row's own
   color — so the row lights as one thing rather than as a lit name beside a
   mark that stayed grey. `inherit` rather than naming --pf-color-ink: each of
   these three states already sets the row's color, and the icon should
   follow whatever that is instead of keeping its own copy of the answer.

   :focus-visible alongside :hover, because a row reached by keyboard is the
   current row in every sense that matters. */
.group-list button.active > ui-icon,
.group-list button:hover > ui-icon,
.group-list button:focus-visible > ui-icon,
.explore-group.active > ui-icon,
.explore-group:hover > ui-icon,
.explore-group:focus-visible > ui-icon {
  color: inherit;
}

.workspace .projects-page {
  flex: 1;
  min-width: 0;
}

.token-sidebar {
  width: 220px;
  flex-shrink: 0;
  border-right: 1px solid var(--pf-color-border);
  background: var(--pf-color-surface-sunken);
  padding: var(--pf-space-4) 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
}

.token-sidebar .sidebar-title {
  font-size: var(--pf-text-sm);
  font-weight: var(--pf-weight-medium);
  color: var(--pf-color-ink);;
  text-transform: capitalize;
  letter-spacing: var(--pf-tracking-loose);
  padding-inline: var(--pf-space-5);
  padding-block: var(--pf-space-3) ;
}

.token-sidebar .sidebar-title:not(.app-sidebar__fold) {
  padding-top: 0;
}


/* ------------------------------------------------------------ user menu --- */

.user-menu {
  position: relative;
}

.user-menu__trigger {
  position: relative;
  display: flex;
  align-items: center;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
}

/* The one deliberate curve in an otherwise square system — an account avatar
   is a strong enough convention to earn the exception. --pf-radius-full is
   otherwise reserved for window-chrome dots. */
.user-menu__avatar {
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border-radius: var(--pf-radius-full);
  background: var(--pf-color-primary);
  color: var(--pf-color-on-primary);
  font-family: var(--pf-font-display);
  font-size: var(--pf-text-sm);
  font-weight: var(--pf-weight-semibold);
  line-height: 1;
}

/* Unread mark. Sits on the tile's top-right corner rather than in the
   menu, so you do not have to open anything to know there is mail.
   Absolute, so showing it cannot reflow the bar. --pf-space-2 is 8px:
   --pf-space-1 disappears on a 26px tile, and anything larger stops
   being a pip. The ring is --pf-color-bg so the pip stays a circle
   when it lands on a photograph. */
.user-menu__alert {
  position: absolute;
  top: 0;
  right: 0;
  width: var(--pf-space-2);
  height: var(--pf-space-2);
  border-radius: var(--pf-radius-full);
  background: var(--pf-color-danger);
  box-shadow: 0 0 0 2px var(--pf-color-bg);
  pointer-events: none;
}

/* With a picture the tile stops being a colored chip and becomes a frame:
   the fill would otherwise show through a transparent PNG, and the initial's
   type styles would still apply to nothing. */
.user-menu__avatar--image {
  background: none;
}

.user-menu__avatar img {
  width: 100%;
  height: 100%;
  border-radius: inherit;
  /* Stored square already, but cover means a non-square value from an older
     row or a hand-written API call still fills the circle instead of
     stretching. */
  object-fit: cover;
  display: block;
}

.user-menu__panel {
  position: absolute;
  top: calc(100% + var(--pf-space-2));
  right: 0;
  z-index: 60;
  min-width: 15rem;
  padding: var(--pf-space-2) 0;
  background: var(--pf-color-surface);
  border: 1px solid var(--pf-color-border-strong);
  border-radius: var(--pf-radius-sm);
  box-shadow: var(--pf-shadow-lg);
}

.user-menu__identity {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--pf-space-2) var(--pf-space-4) var(--pf-space-3);
}

.user-menu__name {
  font-size: var(--pf-text-sm);
  font-weight: var(--pf-weight-medium);
  color: var(--pf-color-ink);
}

.user-menu__email {
  font-size: var(--pf-text-xs);
  color: var(--pf-color-muted);
  overflow-wrap: anywhere;
}

.user-menu__group {
  padding-top: var(--pf-space-2);
  border-top: 1px solid var(--pf-color-border);
}

.user-menu__submenu-host {
  position: relative;
}

/* Flies out to the LEFT. The parent panel is already flush with the right
   edge of the viewport, so the conventional right-hand flyout would open
   off-screen. */
.user-menu__panel--sub {
  top: calc(-1 * var(--pf-space-2));
  /* Overlap by the border width so the two panels share one seam instead of
     showing a double rule or floating apart. */
  right: calc(100% - 1px);
  left: auto;
  min-width: 10rem;
}

.user-menu__item--parent[aria-expanded="true"] {
  background: var(--pf-color-surface-sunken);
}

/* Chevron drawn from borders rather than set as a glyph — Outfit has
   no reliable arrow, and this also lets it point left, at the side the
   submenu actually opens on. */
.user-menu__item--parent::after {
  content: "";
  flex: none;
  width: 6px;
  height: 6px;
  margin-left: auto;
  border-left: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg);
  opacity: 0.55;
}

.user-menu__item {
  display: flex;
  align-items: center;
  gap: var(--pf-space-2);
  width: 100%;
  padding: var(--pf-space-2) var(--pf-space-4);
  border: none;
  border-radius: var(--pf-radius-sm);
  background: none;
  color: var(--pf-color-ink);
  font-family: var(--pf-font-body);
  font-size: var(--pf-text-sm);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}

/* Same 16px marks as the sidebar, muted so the word stays the subject.
   Theme Settings has none: its three choices carry the sun, moon and
   desktop. */
.user-menu__item ui-icon {
  flex: none;
  color: var(--pf-color-muted);
}

.user-menu__item:hover {
  background: var(--pf-color-surface-sunken);
}

/* The selected row is the token sidebar's active row, borrowed: the hover
   ground it already has, a thin rule in the brand green, and medium weight.
   No marker in the content, which also sidesteps Outfit having no check glyph
   — a ✓ here falls back to something resembling a square root.

   On the RIGHT, where the sidebar's is on the left, because that is the edge
   this panel is attached by: it flies out leftward and overlaps the parent's
   border by 1px (see --sub above), so the rule lands on the seam the two
   panels share and reads as part of the menu rather than as a mark floating
   inside it.

   Flat on that side for the same reason. A 2px rule easing into the 4px
   radius the row otherwise carries reads as a smudge at one end, and the
   corner is squared on every choice row rather than only the selected one, so
   the three keep one shape. */
.user-menu__item--choice {
  border-radius: 0;
  border-left: 2px solid transparent;
}

.user-menu__item--choice[aria-checked="true"] {
  background: var(--pf-color-surface-sunken);
  border-left-color: var(--pf-color-primary);
  font-weight: var(--pf-weight-medium);
}

/* ------------------------------------------------- buttons, app-flavoured --- */

/* .btn means two different things in this codebase: a 10px/20px inline-block
   on the marketing pages (site.css) and an 8px/16px inline-flex with a gap
   for its icon in the workspace (app.css). The chrome above belongs to the
   workspace whichever page it is sitting on, so it takes the app shape —
   scoped, because site.css declares its own .btn at the same specificity and
   loads after this sheet, so an unscoped rule here would simply lose.

   Only the shape is restated. site.css marks its .btn-primary and .btn-ghost
   colors !important, which no amount of specificity would beat, but those
   values are identical to app.css's, so there is nothing to fight over.

   .nav-menu--app, not .nav-menu: the marketing header's panel is in the same
   DOM on these pages and keeps the site shape for its Get started button. */
:is(.topbar, .nav-menu--app, .settings) .btn {
  display: inline-flex;
  align-items: center;
  gap: var(--pf-space-2);
  padding: var(--pf-space-2) var(--pf-space-4);
  font-family: var(--pf-font-body);
  background: none;
}

/* Restated at the scoped specificity for the same reason .btn is: without
   these the rule above would strip the fill off a primary button, since it
   now outranks the unscoped .btn-primary that would have restored it. */
:is(.topbar, .nav-menu--app, .settings) .btn-primary {
  background: var(--pf-color-primary);
}

:is(.topbar, .nav-menu--app, .settings) .btn-primary:hover {
  background: var(--pf-color-ink);
}

:is(.topbar, .nav-menu--app, .settings) .btn-inverted {
  background: var(--pf-color-ink);
  color: var(--pf-color-bg);
}

:is(.topbar, .nav-menu--app, .settings) .btn-inverted:hover {
  background: var(--pf-color-primary);
  color: var(--pf-color-on-primary);
}

:is(.topbar, .nav-menu--app, .settings) .btn:disabled {
  opacity: 0.5;
  cursor: default;
  pointer-events: none;
}

/* Neither of these exists in site.css at all, so on a public page the button
   would fall back to the base .btn without them. */
:is(.topbar, .nav-menu--app, .settings) .btn-sm {
  padding: var(--pf-space-1) var(--pf-space-2);
  font-size: var(--pf-text-xs);
  gap: var(--pf-space-1);
}

:is(.topbar, .nav-menu--app, .settings) .btn-danger {
  color: var(--pf-color-danger);
  border-color: var(--pf-color-border-strong);
}

:is(.topbar, .nav-menu--app, .settings) .btn-danger:hover {
  background: var(--pf-color-danger-soft);
  border-color: var(--pf-color-danger);
}

/* ------------------------------ settings dialog --- */

/* Native <dialog>: focus trapping, ESC, the top layer and an inert
   background come from showModal(), so none of it is reimplemented here —
   the same reasoning as the modal component in components/ui/modal. */
.settings {
  width: min(30rem, calc(100vw - var(--pf-space-6)));
  padding: 0;
  border: 1px solid var(--pf-color-border-strong);
  border-radius: var(--pf-radius-sm);
  background: var(--pf-color-surface);
  color: var(--pf-color-ink);
}

.settings::backdrop {
  background: rgb(0 0 0 / 0.45);
}

.settings__panel {
  display: flex;
  flex-direction: column;
  padding: var(--pf-space-6);
}

.settings h2 {
  margin: 0 0 var(--pf-space-5);
  font-size: var(--pf-text-xl);
  font-weight: var(--pf-weight-semibold);
  letter-spacing: -0.02em;
}

.settings label {
    display: block;
    margin-bottom: var(--pf-space-1);
    font-size: var(--pf-text-xs);
    color: var(--pf-color-muted);
    letter-spacing: var(--pf-tracking-loose);
}

.settings textarea,
.settings input[type="text"],
.settings input[type="email"],
/* password too, since the change-password section was added — without it
   those two fields fall back to the browser's default input width and sit
   noticeably narrower than the name and email above them. The visibility
   toggle flips the type to "text" while shown, which this already covers. */
.settings input[type="password"] {
  width: 100%;
  margin-bottom: var(--pf-space-4);
  padding: var(--pf-space-3);
  border: 1px solid var(--pf-color-border-strong);
  border-radius: var(--pf-radius-sm);
  background: none;
  color: var(--pf-color-ink);
  font-family: var(--pf-font-body);
  font-size: var(--pf-text-sm);
}

.settings textarea {
  /* The one field here whose length is unbounded, so it gets to grow — but
     only downward, and only by drag. A textarea that resized horizontally
     would break out of the panel. */
  resize: vertical;
}

.settings textarea:focus,
.settings input:focus {
  outline: none;
  border-color: var(--pf-color-primary);
}

/* Email is shown for orientation but cannot be edited here — it is the JWT's
   identity claim and the key project_shares matches on. */
.settings input:disabled {
  color: var(--pf-color-muted);
  cursor: not-allowed;
}

.settings__identity {
  display: flex;
  align-items: center;
  gap: var(--pf-space-4);
  margin-bottom: var(--pf-space-6);
}

.settings__avatar {
  display: grid;
  place-items: center;
  flex: none;
  width: 64px;
  height: 64px;
  overflow: hidden;
  border-radius: var(--pf-radius-full);
  background: var(--pf-color-primary);
  color: var(--pf-color-on-primary);
  font-family: var(--pf-font-display);
  font-size: var(--pf-text-2xl);
  font-weight: var(--pf-weight-semibold);
  line-height: 1;
}

.settings__avatar--image {
  background: none;
}

.settings__avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.settings__avatar-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--pf-space-2);
}

.settings__hint {
  flex-basis: 100%;
  margin: 0;
  font-size: var(--pf-text-xs);
  color: var(--pf-color-muted);
}

.settings__actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--pf-space-3);
  margin: 0;
  padding: 0;
}

/* Its own block between the save row and the danger zone, separated the same
   way — a rule above it — so the dialog reads as three things you can do
   rather than one long form. */
.settings__password {
  display: block;
  margin-top: var(--pf-space-6);
  padding-top: var(--pf-space-5);
  border-top: 1px solid var(--pf-color-border);
}

.settings__password > strong {
  display: block;
  margin-bottom: var(--pf-space-4);
  font-size: var(--pf-text-sm);
}

/* The dialog's inputs already carry margin-bottom; the wrapper the toggle
   button needs has to take it over, exactly as on the auth forms. */
.settings__password .password-field {
  position: relative;
  margin-bottom: var(--pf-space-4);
}

.settings__password .password-field input {
  margin-bottom: 0;
  padding-right: var(--pf-space-8);
}

.settings__password .password-field .password-toggle {
  position: absolute;
  top: 0;
  right: 0;
  display: flex;
  align-items: center;
  height: 100%;
  padding: 0 var(--pf-space-3);
  border: none;
  background: none;
  color: var(--pf-color-muted);
  cursor: pointer;
}

.settings__password .password-field .password-toggle:hover {
  color: var(--pf-color-ink);
}

/* Set apart, below the save actions and behind its own rule: deleting an
   account is not one of the form's fields and should not read as one. */
/* ---- project settings: the cover image ---- */

.settings__cover {
  display: flex;
  align-items: flex-start;
  gap: var(--pf-space-4);
}

/* The card's own shape, so the preview is the crop being chosen rather than
   an approximation of it. */
.settings__cover-preview {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 11rem;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border: 1px solid var(--pf-color-border);
  border-radius: var(--pf-radius-sm);
  background: var(--pf-color-surface-sunken);
  font-size: var(--pf-text-xs);
  color: var(--pf-color-faint);
}

.settings__cover-preview img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.settings__cover-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--pf-space-2);
}

/* The shared color field, standing on its own in a settings panel rather than
   in a token row's grid. It has no width of its own there, so it is given one
   and the same bottom margin every other field in the panel carries. */
.settings__panel .color-field {
  display: flex;
  width: 100%;
  margin-bottom: var(--pf-space-2);
}

/* A hint belonging to the field above it rather than to a row beside it, so it
   sits tight under its own control and clear of the next label. */
.settings__panel > .settings__hint {
  margin-bottom: var(--pf-space-4);
}

/* Import dialog. It reuses the settings panel wholesale, so what is here is
   only the two things it adds. */

/* The only field in the app that holds source rather than prose. Monospaced
   because the alignment IS the information here — a column of custom
   properties is unreadable in a proportional face — and nowrap because a
   wrapped declaration reads as two. */
.settings textarea.import__source {
  font-family: var(--pf-font-mono);
  font-size: var(--pf-text-xs);
  line-height: var(--pf-leading-body);
  white-space: pre;
  overflow-wrap: normal;
  overflow-x: auto;
}

/* The file picker sits under the textarea, on the same line as the name of
   whatever was picked — proximity rather than a box, since the two are one
   statement: this is the file you chose. */
.import__file {
  display: flex;
  align-items: center;
  gap: var(--pf-space-3);
  margin-bottom: var(--pf-space-4);
}

/* The chosen filename is the subject of that line, so it holds the contrast
   the hint next to the cover picker does not. flex-basis is reset because
   .settings__hint claims a whole row and this one shares. */
.import__file .settings__hint {
  flex-basis: auto;
  color: var(--pf-color-ink);
}

/* An import replaces. Stated in the accent that means "this is destructive"
   everywhere else, and inline with its icon rather than in a panel of its own:
   a warning that needs a box around it has usually failed to be legible. */
.settings__hint--warning {
  display: flex;
  align-items: center;
  gap: var(--pf-space-2);
  color: var(--pf-color-danger);
}

.settings__danger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--pf-space-4);
  margin-top: var(--pf-space-6);
  padding-top: var(--pf-space-5);
  border-top: 1px solid var(--pf-color-border);
}

.settings__danger strong {
  font-size: var(--pf-text-sm);
}

.settings__danger p {
  margin: var(--pf-space-1) 0 0;
  font-size: var(--pf-text-xs);
  color: var(--pf-color-muted);
  max-width: 24ch;
}

.settings .form-error {
  display: none;
  margin: 0 0 var(--pf-space-4);
  padding: var(--pf-space-3) var(--pf-space-4);
  background: var(--pf-color-danger-soft);
  border-left: 2px solid var(--pf-color-danger);
  color: var(--pf-color-danger);
  font-size: var(--pf-text-sm);
}

.settings .form-error.visible {
  display: block;
}

/* Leave: the two-step delete-account flow (see leaveDialog.mjs). Dressed as
   `.settings` so fields and buttons already match; the extra rules are the
   slide and the copy, nothing else. Overflow clips the off-screen pane so
   the dialog's width is one step, never both. */
.leave {
  overflow: hidden;
}

.leave__panel {
  margin: 0;
  padding: 0;
  overflow: hidden;
}

.leave__viewport {
  overflow: hidden;
  width: 100%;
}

.leave__track {
  display: flex;
  /* Width comes from the two panes, which JS sizes to the viewport.
     Percentages here cannot be resolved against the track they live on. */
  transition: transform var(--pf-duration-slow) var(--pf-ease-out);
}

.leave__step {
  box-sizing: border-box;
  flex: none;
  display: flex;
  flex-direction: column;
  gap: var(--pf-space-4);
  padding: var(--pf-space-6);
}

.leave h2 {
  margin: 0;
}

.leave__message {
  margin: 0;
  color: var(--pf-color-muted);
  font-size: var(--pf-text-sm);
  line-height: var(--pf-leading-body);
}

/* The address they have to type: the one piece of the sentence that is
   not supporting copy. */
.leave__message span {
  color: var(--pf-color-ink);
  font-weight: var(--pf-weight-medium);
}

.leave textarea,
.leave input[type="email"] {
  margin-bottom: 0;
}

.leave textarea {
  field-sizing: content;
  min-height: 6lh;
  max-height: 14lh;
  line-height: var(--pf-leading-body);
}

/* Both panes share a height (the track is as tall as the taller step), so
   the menus sit on one line and the dialog does not jump when it slides. */
.leave .settings__actions {
  margin-top: auto;
  padding-top: var(--pf-space-2);
}

.leave .form-error {
  margin-bottom: 0;
}

/* ---------------------------------------------------------------- toast --- */

/* Travels with the chrome: both things that raise one — Copy MCP token and
   the settings dialog — are in the menus above. The element is created on
   demand by utilities/toast.mjs, so a host page needs no markup for it. */
#toast {
  position: fixed;
  bottom: var(--pf-space-6);
  left: 50%;
  transform: translateX(-50%);
  background: var(--pf-color-ink);
  color: var(--pf-color-bg);
  padding: var(--pf-space-3) var(--pf-space-5);
  font-size: var(--pf-text-sm);
  box-shadow: var(--pf-shadow-lg);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--pf-duration-slow) var(--pf-ease-out);
  z-index: 10;
}

#toast.visible {
  opacity: 1;
}

/* A picture replaces the tint entirely, so the tile does not glow through a
   transparent PNG. After the [data-avatar-tint] rules in tokens.css, which
   it has to outrank at equal specificity, so this sheet must load after that
   one — the head partial puts it there. (.share__avatar--image is the third
   of these; it stays in app.css, since the share dialog does not travel.) */
.user-menu__avatar--image,
.settings__avatar--image {
  background: none;
}
