/* ============================================================
 * Layout — app shell, sidebar, page headers, lists
 * 12 columns, 32px gutter (16px mobile), max width 1280px.
 * Breakpoints: mobile < 640 · tablet 640-1024 · desktop > 1024.
 * ============================================================ */

.app-layout {
  display: grid;
  grid-template-columns: var(--sidebar-w) minmax(0, 1fr);
  align-items: start;
  /* align-content must be explicit: min-height creates spare space that
   * the default `normal` would hand to the implicit auto rows, which
   * pushes the stacked mobile layout hundreds of pixels down the page. */
  align-content: start;
  min-height: 100vh;
}

/* ── Sidebar collapse ───────────────────────────────────────
 * The switch is `data-sidebar` on <html>, set before first paint by the
 * script in the layout head. It has to be readable above the grid, because
 * what collapses is the grid's first column and not something inside the
 * sidebar: hiding the aside on its own would leave a 240px gap. */
:root[data-sidebar="collapsed"] .app-layout {
  grid-template-columns: 0 minmax(0, 1fr);
}

/* `visibility: hidden` and not `display: none`. The column is already zero
 * wide, and keeping the aside in the box tree is what lets its width
 * transition instead of snapping - while still taking it out of the tab order
 * and away from screen readers, which `overflow: hidden` alone would not. */
:root[data-sidebar="collapsed"] .app-sidebar {
  width: 0;
  padding-inline: 0;
  border-right-color: transparent;
  visibility: hidden;
}

/* No transition on the collapse. Width and padding are layout properties, so
 * animating them reflows the whole grid on every frame - and the thing being
 * reflowed is a document several thousand pixels tall. A snap is also what the
 * rest of this interface does: nothing here moves except the one spinner that
 * means work is in flight. */

/* Each half of the toggle exists only in the state where it is the way out. */
.sidebar-reveal {
  display: none;
}

:root[data-sidebar="collapsed"] .sidebar-reveal {
  display: inline-flex;
  position: fixed;
  top: var(--space-4);
  left: var(--space-4);
  /* Above the sidebar's own layer, so it is still reachable in the frame the
   * collapse is animating through. */
  z-index: 41;
  /* Squared and sized explicitly. It is the only way back to the sidebar, so
   * unlike the icon buttons that sit inside a row of other affordances it has
   * to be a target on its own - 40px here, and the platform minimum wherever
   * the pointer is a finger. */
  width: 40px;
  height: 40px;
  border-color: var(--border-default);
  border-radius: var(--radius-xs);
  background: var(--bg-surface);
}

@media (pointer: coarse) {
  :root[data-sidebar="collapsed"] .sidebar-reveal {
    width: var(--target-touch);
    height: var(--target-touch);
  }
}

:root[data-sidebar="collapsed"] .app-sidebar__collapse {
  display: none;
}

/* The reveal button is fixed over the page's own top-left, which on this
 * layout is where the breadcrumb starts. */
:root[data-sidebar="collapsed"] main.app-shell {
  padding-left: calc(var(--gutter) + 40px + var(--space-3));
}

main.app-shell {
  width: min(100%, calc(var(--page-width) + 2 * var(--gutter)));
  margin: 0 auto;
  padding: var(--space-10) var(--gutter) var(--space-12);
}

/* minmax(0, 1fr) for the same reason .card and .app-layout's mobile
 * breakpoint need it: an implicit auto column's minimum is its widest
 * child's max-content, so a page section holding a card whose own content
 * runs wide (an un-wrapped stat row, a rename bar) would otherwise size
 * this track - and bleed past it, since this element has no overflow
 * clipping of its own - to fit that child instead of the viewport. */
.page-section {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-8);
}

/* ── Breadcrumb ─────────────────────────────────────────────
 * The only way back up: the sidebar tree shows where a screen sits
 * but never how to leave it. Sits above a page's own header. */

.breadcrumb {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-body-sm);
  color: var(--text-secondary);
}

.breadcrumb__link {
  color: var(--link-color);
  text-decoration: none;
}

.breadcrumb__link:hover {
  text-decoration: underline;
}

.breadcrumb__current {
  color: var(--text-primary);
}

/* neutral-400 is decorative-only in this palette, which is exactly what a
 * separator is - the crumbs themselves carry the contrast. */
.breadcrumb__sep {
  color: var(--neutral-400);
}

.page-header {
  display: grid;
  gap: var(--space-2);
}

/* A page whose header is a step in a task rather than an arrival: the title
 * still names the screen, at h2 rather than at the 48px display size, and the
 * eyebrow and generic intro that went with the full treatment are gone. */
.page-header--compact {
  gap: var(--space-2);
}

.page-header--compact h1 {
  font-size: var(--text-h2);
}

.page-header--workspace {
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: end;
  gap: var(--space-6);
}

/* Element-level only, deliberately. The rule that used to live here styled
 * every .muted-text inside this wrapper at 18rem and ragged-right - written
 * for the grey create hint a real button has replaced. Being a descendant
 * selector it did not stop at the trigger: it reached through into the dialog
 * the trigger opens and set its description to half width, which is why that
 * disclosure had to sit outside this wrapper. A slot that holds a button
 * whose subtree is a whole dialog cannot style by descent. */
.page-header__actions {
  display: flex;
  justify-content: flex-end;
}

.section-heading-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
}

/* ── Projects index, no projects yet ────────────────────────
 * The three steps of the product, in order, with the create button under
 * them. A rule above rather than a card around: the page has no other
 * container, and a boxed empty state reads as a warning. The number is a
 * shape with a border, not only a colour, so it survives forced colours. */
.projects-empty {
  display: grid;
  gap: var(--space-6);
  padding-top: var(--space-6);
  border-top: 1px solid var(--border-default);
}

.projects-empty__heading {
  font-size: var(--text-h4);
}

.projects-empty__steps {
  display: grid;
  gap: var(--space-5);
  margin: 0;
  padding: 0;
  list-style: none;
}

.projects-empty__step {
  display: grid;
  grid-template-columns: 2rem minmax(0, 1fr);
  align-items: start;
  gap: var(--space-4);
}

.projects-empty__step-number {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  border: 1px solid var(--border-control);
  border-radius: 50%;
  color: var(--text-primary);
  font-size: var(--text-body-sm);
  font-weight: var(--weight-medium);
}

.projects-empty__step-title {
  margin: 0 0 var(--space-1);
  color: var(--text-primary);
  font-weight: var(--weight-medium);
}

.projects-empty__action {
  display: flex;
}

/* ── Workspace slabs ────────────────────────────────────── */

.workspace-slab {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-6);
  padding: var(--space-6) 0;
  border-top: 1px solid var(--border-default);
}

.workspace-slab--compact {
  padding: var(--space-4) 0;
}

.workspace-slab:first-of-type {
  border-top: 0;
  padding-top: 0;
}

.workspace-slab__header {
  display: grid;
  gap: var(--space-2);
}

/* A <summary> is a list-item box and draws its marker before its first line,
 * so a block-level heading inside one pushes that line down and leaves the
 * triangle sitting alone above the title. Inline-block puts them on the same
 * line. The heading has to stay a heading - a summary takes phrasing content
 * or exactly one heading, and this one carries the category's count. */
.context-category > summary h2 {
  display: inline-block;
}

/* ── Resource lists ─────────────────────────────────────── */

.resource-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-2);
}

/* `position: relative` is the containing block for the stretched link below.
 * It is on the row and not on the title, because the whole point is that the
 * overlay reaches the row's edges and not the title's. */
.resource-row {
  position: relative;
  display: grid;
  gap: var(--space-1);
  padding: var(--space-4) 0;
  border-bottom: 1px solid var(--border-default);
  color: var(--text-primary);
  transition: background-color var(--motion-base) var(--ease-standard);
}

/* On the row and no longer on `a.resource-row`: the row stopped being one
 * enormous link - its accessible name was the title, the word "Abrir" and the
 * entire description - and the highlight went with it. It is worth keeping
 * without the link. A row this tall, with a title on one line and a paragraph
 * under it, needs something that says which entry the eye and the pointer are
 * on; that is the same job a table row's hover does, and no row of data in
 * this product had one. */
.resource-row:hover {
  background: var(--bg-subtle);
}

.resource-list > li:last-child .resource-row {
  border-bottom: 0;
}

.resource-row__title-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
}

.resource-row__title {
  font-size: var(--text-body);
}

/* The title is a link again, and taking the link colour turned the whole
 * project list blue. In a list of one kind of thing, where every row leads to
 * the same kind of place, colouring every title says nothing the list did not
 * already say - it just makes the page louder. The affordance is carried by
 * the underline on hover and by the focus ring, both of which the base link
 * rules still give it. */
a.resource-row__title {
  color: var(--text-primary);
}

/* ── The clickable row ──────────────────────────────────────
 * The pointer target was a project name on the left of a 1280px row and the
 * word "Abrir" at the far right of it, about 1,100px apart, with the whole
 * band between them dead. This puts the target back on the row.
 *
 * A stretched pseudo-element and not an <a> around the row. The row was one
 * enormous link once and stopped being one on purpose - see the :hover comment
 * above - because the accessible name it produced was the title, the word
 * "Abrir" and the entire description read out before the next row began. The
 * ::after grows the POINTER target without touching the accessible name, which
 * stays the project name the anchor actually wraps.
 *
 * No CONTROL needs lifting above it. The only other one in the row is the
 * decorative "Abrir", which is aria-hidden, out of the tab order and points
 * where the title points; being unreachable under the overlay is what it
 * should be. If a row ever gains a second, real action, that one needs
 * `position: relative` and a z-index of its own to sit above this.
 *
 * TEXT is the other half of that sentence and it does need lifting - see
 * .resource-row__meta below. An overlay that takes the pointer over the whole
 * row takes it over the prose too, and a description that cannot be dragged
 * over is a description that cannot be copied. Screen readers were never
 * affected (the text is in the accessibility tree either way); what the
 * overlay broke was selection with a mouse. */
.resource-row__link::after {
  content: "";
  position: absolute;
  inset: 0;
}

/* The row is what got hovered and the row is what lights up, which
 * .resource-row:hover above already does. Underlining the title as well is the
 * cue that the row leads somewhere rather than merely being pointed at. */
.resource-row:hover .resource-row__link {
  text-decoration: underline;
}

/* Focus reads on the row, for the same reason the click target is the row:
 * a ring around eight characters of a title, in a band the pointer treats as
 * one object, describes the wrong thing. Same two-pixel primary-500 ring and
 * same offset as every other focused element - see the :focus-visible rule in
 * 02_base.css - only drawn around a bigger box.
 *
 * The offset is negative here and positive everywhere else: rows in this list
 * are separated by a one-pixel rule and nothing more, so a ring outside the
 * box would be drawn over the neighbours above and below. */
.resource-row:has(.resource-row__link:focus-visible) {
  outline: 2px solid var(--focus-ring);
  outline-offset: -2px;
  border-radius: var(--radius-xs);
}

/* The row's ring replaces the title's; two concentric rings around the same
 * focus is one more than tells the reader anything. */
.resource-row__link:focus-visible {
  outline: none;
}

/* Capped at the reading measure. Written here rather than inline on the tag,
 * and without the --measure fallback the inline version needed: the token has
 * landed in 01_tokens.css.
 *
 * `position: relative` is the one thing the row's click overlay has to give
 * back. Measured on /projects: with the paragraph static, elementFromPoint at
 * the middle of the description returned the anchor, so dragging across the
 * text selected nothing at all. Positioned, it paints and hit-tests above the
 * ::after and the selection works again.
 *
 * No z-index on purpose. Both are in the same stacking context at `auto`, so
 * paint order decides, and the paragraph comes after the anchor in the DOM -
 * which is exactly as much lift as is wanted. A z-index would raise the whole
 * paragraph BOX, and its box is the full row width while its text is capped at
 * --measure, so the empty band to the right of the description would stop
 * being clickable. This way only the glyphs take the pointer. */
.resource-row__meta {
  position: relative;
  max-width: var(--measure);
  color: var(--text-secondary);
  font-size: var(--text-body-sm);
}

/* ── Sidebar ────────────────────────────────────────────────
 * 240px expanded. One-line items 32px tall, radius 6px.
 * Nav items 40px with 16px/8px padding. */

.app-sidebar {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
  position: sticky;
  top: 0;
  z-index: 40;
  height: 100vh;
  /* Not `auto`. The menu below is the scroll region - that is what keeps the
   * brand and the theme/locale footer in place while the project tree moves -
   * and a second `auto` here only ever produced a second scrollbar for the
   * same overflow. */
  overflow: hidden;
  padding: var(--space-5) var(--space-4);
  border-right: 1px solid var(--border-default);
  background: var(--bg-surface);
}

.app-sidebar__topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  min-height: var(--sidebar-item-h);
}

.app-sidebar__brand-link {
  display: inline-flex;
  align-items: center;
  color: var(--text-primary);
  font-size: var(--text-h4);
  letter-spacing: var(--tracking-tight);
}

.app-sidebar__brand-link:hover {
  text-decoration: none;
}

.app-sidebar__quick-actions {
  display: flex;
  align-items: center;
  gap: var(--space-1);
}

.app-sidebar__quick-action-popover,
.app-sidebar__inline-popover {
  position: relative;
}

.app-sidebar__quick-action-popover > summary,
.app-sidebar__inline-popover > summary {
  list-style: none;
}

/* The nav is a <details> so it can collapse on tablet and phone. On
 * desktop the toggle is hidden and sidebar-menu keeps it open. */
.app-sidebar__menu-toggle {
  display: none;
}

.app-sidebar__menu {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  min-height: 0;
  overflow-y: auto;
  /* `clip` and not `hidden`: declaring `overflow-y: auto` on its own leaves
   * this axis `visible`, and the spec then computes `visible` to `auto` -
   * which is where the sidebar's horizontal scrollbar came from. The tree
   * below can no longer overflow, so nothing is being hidden here; this keeps
   * the next long project name from bringing the bar back. `clip` is the
   * variant that does not turn the other axis into a scroll container. */
  overflow-x: clip;
}

/* Overriding `display` on a <details> leaves the UA slot that holds its
 * contents in the box tree as an empty flex item, so a closed disclosure still
 * pays the gap between the summary and nothing at all - 16px of dead space
 * under "Menú" on every phone. Measured: the closed toggle is 44px and this
 * element was 60px. Same defect on .app-settings-menu, fixed the same way in
 * 03_components.css. */
.app-sidebar__menu:not([open]) {
  gap: 0;
}

.app-sidebar__nav {
  display: grid;
  gap: var(--space-1);
}

/* Nav item: 40px, 16px/8px padding, radius 6px. */
.app-sidebar__nav-link {
  display: flex;
  align-items: center;
  min-height: var(--nav-item-h);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-xs);
  color: var(--text-secondary);
  font-size: var(--text-body-sm);
  transition: background-color var(--motion-base) var(--ease-standard),
    color var(--motion-base) var(--ease-standard);
}

.app-sidebar__nav-link:hover {
  background: var(--bg-subtle);
  color: var(--text-primary);
  text-decoration: none;
}

/* Active state carries a fill and the primary hue, not just weight,
 * so it does not depend on colour alone. */
.app-sidebar__nav-link.is-active {
  background: var(--info-bg);
  color: var(--link-color);
}

.app-sidebar__tree {
  display: grid;
  gap: var(--space-3);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border-default);
}

/* `min-width: 0` on every grid box between the tree and the links. A grid or
 * flex item defaults to `min-width: auto`, which is its min-content width, so
 * one long project name or conversation title set the width of the whole
 * column and pushed it 78px past the sidebar - the `text-overflow: ellipsis`
 * further down could never fire, because nothing was ever narrower than the
 * text. */
.app-sidebar__project-node {
  display: grid;
  gap: var(--space-1);
  min-width: 0;
}

/* One-line sidebar item: 32px tall, radius 6px. */
/* This row is what actually set the sidebar's width. A summary stays laid out
 * while its details is closed, so the longest project name in the workspace -
 * open or not - sized the whole tree column and gave the menu a horizontal
 * scrollbar. `min-width: 0` here and on the title is what lets the ellipsis
 * the title already asks for do its job. */
.app-sidebar__project-summary {
  list-style: none;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
  min-height: var(--sidebar-item-h);
  padding: 0 var(--space-2);
  border-radius: var(--radius-xs);
  color: var(--text-secondary);
  font-size: var(--text-body-sm);
  transition: background-color var(--motion-base) var(--ease-standard),
    color var(--motion-base) var(--ease-standard);
}

.app-sidebar__project-summary:hover {
  background: var(--bg-subtle);
  color: var(--text-primary);
}

.app-sidebar__project-summary.is-active {
  color: var(--text-primary);
}

.app-sidebar__project-summary-title {
  min-width: 0;
  flex: 1 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Never shrinks: it is the only thing that says whether the node is open. */
.app-sidebar__project-summary-icon {
  display: inline-flex;
  flex: 0 0 auto;
  color: var(--text-secondary);
  transition: transform var(--motion-base) var(--ease-standard);
}

.app-sidebar__project-node[open]
  > .app-sidebar__project-summary
  .app-sidebar__project-summary-icon {
  transform: rotate(90deg);
}

/* The tree is two levels deep, and 25px of margin + padding + rule for one
 * level was charging the rows inside it more than the nesting was worth: the
 * estimate dates were being ellipsised to pay for it. 17px still reads as
 * indented under the project, against the same rule. */
.app-sidebar__project-children {
  display: grid;
  gap: var(--space-3);
  margin-left: var(--space-2);
  padding-left: var(--space-2);
  border-left: 1px solid var(--border-default);
}

.app-sidebar__tree-group {
  display: grid;
  gap: var(--space-1);
  min-width: 0;
}

.app-sidebar__tree-group-toggle {
  list-style: none;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
  min-height: var(--sidebar-item-h);
  padding: 0 var(--space-2);
  border-radius: var(--radius-xs);
  cursor: pointer;
  transition: background-color var(--motion-base) var(--ease-standard);
}

.app-sidebar__tree-group-toggle:hover {
  background: var(--bg-subtle);
}

.app-sidebar__tree-group-toggle .icon {
  flex: 0 0 auto;
  color: var(--text-secondary);
  transition: transform var(--motion-base) var(--ease-standard);
}

.app-sidebar__tree-group[open] > .app-sidebar__tree-group-toggle .icon {
  transform: rotate(90deg);
}

.app-sidebar__thread-list,
.app-sidebar__estimate-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-1);
  min-width: 0;
}

.app-sidebar__thread-list > li,
.app-sidebar__estimate-list > li {
  min-width: 0;
}

.app-sidebar__tree-link,
.app-sidebar__thread-link,
.app-sidebar__estimate-link {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  min-height: var(--sidebar-item-h);
  padding: 0 var(--space-2);
  border-radius: var(--radius-xs);
  color: var(--text-secondary);
  font-size: var(--text-body-sm);
  transition: background-color var(--motion-base) var(--ease-standard),
    color var(--motion-base) var(--ease-standard);
}

.app-sidebar__thread-link > span:first-child,
.app-sidebar__estimate-link > span:first-child {
  /* `min-width: 0` for the same reason as the boxes above: without it this
   * flex item cannot shrink under its own nowrap text, and the ellipsis it
   * asks for never has a narrower box to appear in. */
  min-width: 0;
  flex: 1 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The badge is the half of the row that must stay whole. A truncated date is
 * still a date; a truncated mode is a different word, and on the status
 * variant it is the difference between "En proceso" and "Completada". */
.app-sidebar__thread-link > .badge,
.app-sidebar__estimate-link > .badge {
  flex: 0 0 auto;
}

/* All the estimate badges to one width, set by the longest of them
 * ("Aproximada"). Without it every row's date got a different amount of room
 * and the column read as ragged - one date whole, the next ellipsised, for no
 * reason a reader could see. */
.app-sidebar__estimate-link > .badge {
  /* Sized to "Aproximada", the longest of them, with the tighter of the two
   * badge paddings - every 2px here comes off the date beside it. */
  min-width: 4.9rem;
  padding-inline: var(--space-2);
  justify-content: center;
}

/* A count, not a sentence: sized to two digits so it stays a dot of
 * information beside the title rather than competing with it.
 *
 * The min-width is what keeps the badge from resizing as the count grows, so
 * there was nothing left for tabular figures to do here - and in PP Neue
 * Montreal they are 25% wider than the proportional set, which printed "11"
 * as "1 1" inside a badge two characters wide. See __estimate-when below. */
.app-sidebar__count {
  min-width: 1.75rem;
  padding-inline: var(--space-2);
  justify-content: center;
}

.app-sidebar__tree-link:hover,
.app-sidebar__thread-link:hover,
.app-sidebar__estimate-link:hover {
  background: var(--bg-subtle);
  color: var(--text-primary);
  text-decoration: none;
}

.app-sidebar__tree-link.is-active,
.app-sidebar__thread-link.is-active,
.app-sidebar__estimate-link.is-active {
  background: var(--info-bg);
  color: var(--link-color);
}

/* These rows used to ask for tabular figures, on the grounds that a column of
 * timestamps needs the same digit under itself down the list. Measured, that
 * premise does not hold for this content: the stamps are "1 sep", "11 sep
 * 14:45", "3 oct 9:05" - variable-length, with a month name in the middle - so
 * no digit was ever going to line up with the one above it whatever the figure
 * set did. What tabular did deliver is PP Neue Montreal's tabular width, 25%
 * over the proportional set, which is why the sidebar read "1 1 sep 1 4:45".
 *
 * Nothing declared here now: the rows take the proportional default. The rule
 * stays as the place this decision is written down. */
.app-sidebar__estimate-when {
  font-variant-numeric: normal;
}

/* Reads as the last row of the list it closes rather than as a peer of the
 * estimates above it, so the eye stops at the estimates and finds this only
 * when none of them was the one. */
.app-sidebar__tree-link--more {
  color: var(--text-secondary);
  font-size: var(--text-body-sm);
}

.app-sidebar__tree-group-toggle > .eyebrow,
.app-sidebar__tree > .eyebrow {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.app-sidebar__empty-note {
  margin: 0;
  padding: 0 var(--space-2);
  color: var(--text-secondary);
  font-size: var(--text-body-sm);
}

.app-sidebar__project-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.app-sidebar__inline-action {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--sidebar-item-h);
  padding: 0 var(--space-2);
  border: 0;
  border-radius: var(--radius-xs);
  background: transparent;
  color: var(--text-secondary);
  font-size: var(--text-body-sm);
  transition: background-color var(--motion-base) var(--ease-standard),
    color var(--motion-base) var(--ease-standard);
}

.app-sidebar__inline-action:hover {
  background: var(--bg-subtle);
  color: var(--text-primary);
  text-decoration: none;
}

/* Footer holds the settings menu - theme and language, both folded away. It
 * is one row tall closed, which is the point: on phone the sidebar stacks
 * above the content, so every pixel here is a pixel of the first screen. */
.app-sidebar__footer {
  display: grid;
  gap: var(--space-2);
  margin-top: auto;
  padding-top: var(--space-4);
  border-top: 1px solid var(--border-default);
}

/* The API key's estimated balance: one quiet row above the settings menu.
 * It only changes colour when it runs low, and even then the loud version
 * is the banner over the content - the sidebar folds away. */
.app-sidebar__balance {
  display: grid;
  gap: var(--space-1);
  padding: 0 var(--space-2);
  font-size: var(--text-body-sm);
  color: var(--text-secondary);
}

.app-sidebar__balance-line {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--space-2);
  margin: 0;
}

.app-sidebar__balance-line strong {
  color: var(--text-primary);
  font-weight: var(--weight-book);
  font-variant-numeric: tabular-nums;
}

/* No red text: the error tokens are border and fill only. A warning edge
 * carries the state, and the figure keeps full contrast. */
.app-sidebar__balance--low {
  padding-left: var(--space-2);
  border-left: 2px solid var(--warning-border);
}

.app-sidebar__balance-note {
  margin: 0;
}

.app-sidebar__balance-action {
  justify-self: start;
  color: var(--link-color);
}

/* One line: the sentence, then the action at its end. Flex rather than the
 * .alert grid so the two share a row, and wrap so a narrow screen puts the
 * action under the text instead of squeezing either. Tighter padding than a
 * standard alert: this one is on every page while the balance is low. */
.llm-balance-banner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-1) var(--space-4);
  margin-bottom: var(--space-4);
  padding: var(--space-2) var(--space-4);
}

.llm-balance-banner__text {
  flex: 1 1 20rem;
  margin: 0;
  font-size: var(--text-body-sm);
}

.llm-balance-banner__title {
  font-weight: var(--weight-medium);
}

/* A link and not a button, so it sits in the line as text does, but a 40px
 * target all the same. Underlined: it is on a warning fill, where the link
 * colour alone would be the only thing marking it. */
.llm-balance-banner__action {
  display: inline-flex;
  align-items: center;
  min-height: var(--nav-item-h);
  color: var(--link-color);
  font-size: var(--text-body-sm);
  font-weight: var(--weight-medium);
  text-decoration: underline;
  white-space: nowrap;
}

/* No Neocoast mark in the sidebar. design.md's logo section is a set
 * of usage rules for when you place the logo, not a requirement to
 * place one, and the only logotipo shipped with the design system is
 * the white variant, which that same section reserves for dark or
 * coloured grounds. Adding it here would mean either recolouring the
 * mark (forbidden) or dropping a dark plate into a light UI. Revisit
 * once the colour logotipo is available in the skill assets. */

/* ── Tablet and below: sidebar becomes a drawer ─────────────
 * design.md's tablet band starts at 1024px, which is where the
 * 240px rail stops being affordable. */

@media (max-width: 1024px) {
  /* minmax(0, 1fr), not a bare 1fr: a single-track grid's implicit minimum
   * is auto (its widest child's min-content), so without the 0 floor the
   * track - and every ancestor up to the page - stretches to fit whatever
   * is widest inside it (a breakdown table, a roadmap lane), overriding
   * that content's own overflow-x:auto and forcing the whole page to
   * scroll horizontally instead. */
  .app-layout {
    grid-template-columns: minmax(0, 1fr);
  }

  :root[data-sidebar="collapsed"] .app-layout {
    grid-template-columns: minmax(0, 1fr);
  }

  /* No collapse below the rail. The bar is one 56px row now, so hiding it
   * saves almost nothing, and the way back was a fixed button floating over
   * the page's top-left corner. A "collapsed" choice made on a desktop is
   * still stored and still applies there; here it is simply not honoured. */
  :root[data-sidebar="collapsed"] .app-sidebar {
    width: auto;
    padding-inline: var(--gutter-mobile);
    visibility: visible;
  }

  .app-sidebar__collapse,
  :root[data-sidebar="collapsed"] .sidebar-reveal {
    display: none;
  }

  :root[data-sidebar="collapsed"] main.app-shell {
    padding-left: var(--gutter-mobile);
  }

  /* H54: one row of ~56px - brand, the new-project shortcut, and the menu
   * button. It was ~150px in two ragged rows (brand; Menú, the balance link and
   * Ajustes), and it was sticky, so in a conversation those 150px sat on top
   * of the messages for the whole read. It scrolls away with the page now.
   *
   * Everything else lives behind "Menú": the navigation, the project tree, the
   * balance and the settings. The DOM order is the desktop rail's (topbar,
   * menu, footer) and stays that way; the menu button is lifted into the
   * topbar row with absolute positioning, and the footer only shows while the
   * menu is open, as the last part of it. */
  .app-sidebar {
    position: relative;
    top: auto;
    height: auto;
    overflow: visible;
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 0;
    padding: var(--space-2) var(--gutter-mobile);
    border-right: 0;
    border-bottom: 1px solid var(--border-default);
  }

  /* Room on the right for the menu button that sits over this row. */
  .app-sidebar__topbar {
    min-height: var(--nav-item-h);
    padding-right: calc(var(--space-2) + 6.5rem);
  }

  .app-sidebar__quick-actions .icon-button {
    width: var(--nav-item-h);
    height: var(--nav-item-h);
  }

  .app-sidebar__menu-toggle {
    position: absolute;
    top: var(--space-2);
    right: var(--gutter-mobile);
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    min-height: var(--nav-item-h);
    padding: 0 var(--space-3);
    border: 1px solid var(--border-control);
    border-radius: var(--radius-xs);
    background: var(--bg-surface);
    color: var(--text-primary);
    font-size: var(--text-body-sm);
    list-style: none;
    cursor: pointer;
  }

  .app-sidebar__menu-toggle::-webkit-details-marker {
    display: none;
  }

  .app-sidebar__menu-toggle .icon {
    transition: transform var(--motion-base) var(--ease-standard);
  }

  .app-sidebar__menu[open] > .app-sidebar__menu-toggle .icon {
    transform: rotate(180deg);
  }

  /* Open, the menu is a panel under the row. The tree can be long, so it
   * scrolls inside a cap rather than pushing the page a screen down. */
  .app-sidebar__menu {
    align-items: stretch;
    overflow: visible;
  }

  .app-sidebar__menu[open] {
    max-height: 70vh;
    overflow-y: auto;
    padding-block: var(--space-4) var(--space-2);
  }

  /* Balance and settings belong to the menu here, so they are hidden with it.
   * The desktop rail keeps them pinned at its foot as before. */
  .app-sidebar:has(.app-sidebar__menu:not([open])) .app-sidebar__footer {
    display: none;
  }

  .app-sidebar__footer {
    margin-top: 0;
    padding-block: var(--space-3) var(--space-2);
  }

  .app-sidebar__footer .app-settings-menu__toggle {
    min-height: var(--nav-item-h);
  }

  /* Where the pointer is a finger, the two disclosures get --target-touch,
   * which 01_tokens.css reserves for exactly these. The row grows to ~60px to
   * pay for it; the menu button's offset stays the same. */
  @media (pointer: coarse) {
    .app-sidebar__topbar {
      min-height: var(--target-touch);
    }

    .app-sidebar__menu-toggle,
    .app-sidebar__footer .app-settings-menu__toggle {
      min-height: var(--target-touch);
    }
  }

  main.app-shell {
    padding: var(--space-6) var(--gutter-mobile) var(--space-10);
  }
}

@media (max-width: 640px) {
  .page-header--workspace {
    grid-template-columns: 1fr;
    align-items: start;
  }

  .page-header__actions {
    justify-content: flex-start;
  }

  .card {
    padding: var(--space-5);
  }

  .toast-stack {
    top: var(--toast-top, var(--space-3));
    right: var(--space-3);
    left: var(--space-3);
    width: auto;
  }

  .toast {
    padding: var(--space-4);
  }
}
