/* Grafy navigation drawer — the SINGLE source of truth for the sidebar's
   box, nav rail, and collapse behaviour, shared by the graph app (index.html)
   and every satellite (Social / 3D Studio / Chat / Code via appdrawer.js).
   Before this file the two had separate CSS that drifted, so the same drawer
   behaved differently per surface.

   The rail lives at the INLINE START, not on the left: Grafy ships Arabic,
   Persian and Urdu, and in those the start edge is the right one. Everything
   here that has a side is therefore written logically (`inset-inline-start`,
   `border-inline-end`, `margin-inline-start`, `text-align: start`) so one
   stylesheet lays the drawer out correctly in both directions. Issue #63 was
   what happens when it is not: the whole page mirrored around a rail that
   stayed pinned to the left, with the menu button on top of the tab strip.

   Two things make one stylesheet serve both:
   1. Colours come from `--drawer-*` tokens that fall back to the graph app's
      own tokens. The app defines none, so it keeps its exact look; a satellite
      (which has no --text/--accent of its own) sets the tokens to a fixed dark
      palette on the drawer root.
   2. Collapse/expand state lives on an ANCESTOR class (`drawer-collapsed`,
      `graph-open`, `more-open`) rather than a specific element, so it matches
      whether that class sits on the app's `.shell` or a satellite's <html>.

   Page LAYOUT (how the app's grid or a satellite's body makes room for the
   drawer, and the app's narrow-screen overlay) stays with each host — only the
   drawer's own chrome is shared here. */

.appdrawer {
  --d-fg: var(--drawer-fg, var(--text, #f4f2ed));
  --d-muted: var(--drawer-muted, var(--muted, #979ba4));
  --d-accent: var(--drawer-accent, var(--accent, #aeb7c2));
  --d-line: var(--drawer-line, var(--line, #2b313e));
  --d-line-soft: var(--drawer-line-soft, var(--line-soft, #1f2430));
  --d-raised: var(--drawer-raised, var(--surface-raised, #1b1f2a));
  --d-bg: var(--drawer-bg, color-mix(in srgb, var(--bg, #0b0e15) 90%, #000 6%));
  /* The padlock on a row this plan does not include. Red on purpose: it used
     to be --d-muted, which on a row already dimmed to .5 opacity was a grey
     speck on a grey icon that nobody read as "locked". */
  --d-lock: var(--drawer-lock, var(--danger, #ff6b6b));

  display: flex;
  flex-direction: column;
  min-width: 0;
  height: 100vh;
  max-height: 100vh;
  border-inline-end: 1px solid var(--d-line-soft);
  background: var(--d-bg);
  color: var(--d-fg);
  overflow: hidden;
  /* Own the font so the drawer reads identically on every surface. Without this
     it inherited each host's font (the app's Inter vs a satellite's fallback),
     which is why the nav text looked a different size when you switched pages. */
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  font-size: 14px;
  line-height: 1.4;
}

.appdrawer-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 14px 14px 8px;
}
.appdrawer-brand { display: flex; align-items: baseline; gap: 7px; text-decoration: none; color: var(--d-fg); }
/* The row is baseline-aligned so "Grafy" and the surface name sit on one
   line. The logo has no text baseline of its own, so an empty box would be
   aligned by its bottom edge and ride high above the wordmark -- centre it
   explicitly. Both class names are listed because the graph app writes the
   mark as .brand-mark and the injected drawer as .gd-mark; matching only one
   is why /app's wordmark sat lower than every other surface's. */
.appdrawer-brand .gd-mark,
.appdrawer-brand .brand-mark { align-self: center; }

/* The logo tile itself. This lived in appdrawer.js's injected stylesheet, which
   meant the server-rendered first-paint rail drew a 0x0 mark and its whole top
   block came out 17px short — so every nav row sat 17px high until the real
   drawer landed, and then jumped down. Shared chrome belongs in the shared
   stylesheet: it is the same tile on every surface, and this is the file both
   rails already load. */
.gd-mark {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  width: 38px;
  height: 38px;
  flex: none;
  border: 1px solid color-mix(in srgb, var(--d-accent) 44%, transparent);
  border-radius: 11px;
  background:
    radial-gradient(circle at 50% 50%, color-mix(in srgb, #62c8ff 20%, transparent) 0 28%, transparent 58%),
    linear-gradient(145deg, #222831, #10151c 56%, #0b0d10);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .18), 0 12px 30px rgba(0, 0, 0, .28);
}
.gd-mark::before {
  content: "";
  position: absolute;
  inset: 6px;
  z-index: 1;
  background: url("/icons/graph-galaxy-symbol.svg") center/contain no-repeat;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, .42));
}
.gd-mark::after {
  content: "";
  position: absolute;
  inset: 3px;
  z-index: 0;
  border: 1px solid color-mix(in srgb, var(--d-accent) 18%, transparent);
  border-radius: 9px;
  background: radial-gradient(circle at 50% 50%, color-mix(in srgb, #62c8ff 12%, transparent) 0 18%, transparent 19%);
  opacity: .64;
}

/* A surface whose own top bar carries a "Grafy <app>" wordmark shows it
   twice while the rail is expanded, because the rail says exactly the same
   thing three centimetres to the left. The page's copy stands down and
   returns when the rail collapses to icons, where the surface name is no
   longer readable. Tag the page's own wordmark with .grafy-page-brand. */
html.gd-on:not(.drawer-collapsed) .grafy-page-brand { display: none; }
.appdrawer-brand-name { font-size: 16px; font-weight: 750; }
/* "Grafy <app>" — the surface name smaller and dimmer beside the wordmark. */
.appdrawer-brand-app { font-size: 12.5px; font-weight: 600; color: var(--d-muted); }
.appdrawer-brand-app:empty { display: none; }

.appdrawer-collapse {
  width: 30px; height: 30px; display: grid; place-items: center; padding: 0;
  border: 1px solid transparent; border-radius: 8px; background: transparent;
  color: var(--d-muted); cursor: pointer;
}
.appdrawer-collapse svg { width: 17px; height: 17px; fill: none; stroke: currentColor; stroke-width: 1.7; }
.appdrawer-collapse:hover { border-color: var(--d-line); color: var(--d-fg); }

.appdrawer-nav { flex: 1; min-height: 0; overflow-y: auto; padding: 6px 10px; display: flex; flex-direction: column; gap: 2px; }
.appdrawer-bottom { padding: 8px 10px 12px; border-top: 1px solid var(--d-line-soft); display: flex; flex-direction: column; gap: 2px; }

/* --- The nav's scrollbar --------------------------------------------------
   The rail scrolls, and on Windows that meant a classic ~15px scrollbar with a
   stepper arrow at each end. Ugly is the smaller half of the problem: a classic
   scrollbar takes its width out of the scroller's CONTENT box, so every row in
   .appdrawer-nav was ~15px narrower than the blocks above and below it — and a
   collapsed row CENTRES its icon, so every icon in the rail sat ~7px toward the
   start of the launcher and collapse buttons in .appdrawer-top, which do not
   scroll. The rail looked bent, and no amount of padding fixes it, because the
   width the bar claims is not padding.

   So the bar stops claiming width. The native one is hidden and the scroll
   position is drawn instead as a thin overlay riding the nav's inline-end edge,
   moved by a scroll-driven animation: no script, no arrows, no layout.

   The overlay is a pseudo-element of the WRAPPER, not of the nav. Anything
   painted inside a scroller scrolls with its contents, so a thumb drawn there
   would slide away with the very rows it is reporting on. The wrapper exists
   for that one reason and is otherwise layout-neutral: it takes the flex slot
   the nav used to take and hands it straight back, so a rail that has not been
   wrapped yet measures exactly the same.

   Browsers with no scroll-driven animations are the ones that cannot draw this,
   so they keep a real scrollbar — narrowed and de-arrowed by `scrollbar-width:
   thin`, which is the best a native bar can be asked to do. */
.appdrawer-scroll { position: relative; flex: 1; min-height: 0; display: flex; flex-direction: column; }
.appdrawer-nav { scrollbar-width: thin; scrollbar-color: var(--d-line2, var(--d-line)) transparent; }

@supports (animation-timeline: scroll()) {
  .appdrawer-nav { scrollbar-width: none; }
  .appdrawer-nav::-webkit-scrollbar { width: 0; height: 0; }

  /* The nav names its own scroll progress; timeline-scope lifts that name out
     of the scroller so the wrapper — its parent, and therefore not somewhere a
     descendant's name would normally reach — can drive the overlay with it. */
  .appdrawer-nav { scroll-timeline: --gd-nav-scroll block; }
  .appdrawer-scroll { timeline-scope: --gd-nav-scroll; }

  .appdrawer-scroll::after {
    content: "";
    position: absolute;
    inset-inline-end: 3px;
    top: 6px;
    width: 3px;
    height: 36px;
    border-radius: 3px;
    background: var(--d-fg);
    pointer-events: none;
    /* Invisible at rest is also the right answer for a rail short enough not to
       scroll: with no overflow the timeline is inactive, the animation does not
       apply, and these base styles are what shows. Nothing to scroll, nothing
       drawn — without a line anywhere saying so. */
    opacity: 0;
    animation-name: gd-nav-scroll-ind;
    animation-duration: auto;
    animation-timing-function: linear;
    animation-fill-mode: both;
    animation-timeline: --gd-nav-scroll;
  }
  /* Travel is expressed in `top` because a percentage there resolves against
     the wrapper's height — the one length that is the visible track. A
     translate would resolve against the thumb's own 36px instead. */
  @keyframes gd-nav-scroll-ind {
    from { top: 6px; opacity: .3; }
    to { top: calc(100% - 42px); opacity: .3; }
  }
}

/* A place kept for a row that mounts later (the notification bell, the coach
   replay), so the block is the same height before and after that row lands and
   the scrolling nav above it never gets shorter mid-session.

   The slot IS a .dnav carrying an empty .dnav-ic, and the row that arrives
   REPLACES it. That is the whole trick: it inherits every rule a real row has,
   in both the expanded and the collapsed rail, so its height is right by
   construction rather than by a number written down twice. The first attempt
   here did write it down twice -- `height: 37px` -- and was wrong the moment
   the rail collapsed, where .dnav-ic grows to 24px and the padding narrows,
   making a row 42px. A slot that measures itself cannot drift like that. */
.gd-slot { pointer-events: none; }

/* --- The account row ------------------------------------------------------
   The avatar + name button and the notification bell share ONE line. The bell
   used to be a nav row of its own at the top of this block, which spent a full
   rail row — label and all — on a control that is a glyph and a count, and put
   it above the credits where nothing else about the account sits. Beside the
   name is where it belongs: it is that account's bell.

   Both rails build this shape (the graph app's static markup in index.html and
   the injected one in appdrawer.js), which is why the rule is here and not in
   either. The two use different class names for the button itself — .user-button
   in the app, .gd-account-btn in the drawer — so both are named; each is the
   line's flexible part and the bell is a fixed square at the end.

   The .appdrawer prefix is not decoration: styles.css sets
   `.appdrawer-account .user-button { width: 100% }` and loads AFTER this file,
   so a bare `.gd-account-row > .user-button` would tie on specificity and lose,
   and the button would take the whole line with the bell pushed off it. */
.gd-account-row { display: flex; align-items: center; gap: 6px; width: 100%; min-width: 0; }
.appdrawer .gd-account-row > .gd-account-btn,
.appdrawer .gd-account-row > .user-button { flex: 1 1 auto; min-width: 0; width: auto; }

/* 60px of rail has no room for two controls side by side, so they stack and the
   bell goes back to being an icon on its own line — which is what every other
   control in the collapsed rail already is. */
.drawer-collapsed .gd-account-row { flex-direction: column; align-items: stretch; gap: 2px; }

/* Space kept for the bell until notify-bell.js mounts it — the same bargain
   .gd-slot makes for the rows below, in the shape this one arrives in. It is
   the box the bell wears (.gnb-btn in notify-bell.js), so the row is the size
   it will end up at from the first frame, stacked as well as inline. */
.gd-bell-slot { flex: none; width: 34px; height: 34px; pointer-events: none; }
.drawer-collapsed .gd-bell-slot { width: auto; }

.dnav {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 8px 11px; border: 1px solid transparent; border-radius: 9px;
  background: transparent; color: var(--d-fg); cursor: pointer;
  font-size: 13.5px; font-weight: 600; text-align: start; text-decoration: none;
  transition: gap .2s ease, padding .2s ease, background-color .16s ease, border-color .16s ease;
}
.dnav:hover { background: var(--d-raised); border-color: var(--d-line); }
/* Graph Chat used to be tinted with --d-accent to mark it as the primary app.
   That token (#aeb7c2) is dimmer than --d-fg (#f4f2ed), so the "highlight" read
   as disabled next to every sibling. It now matches them; which surface you are
   on is carried by .gd-active's raised background instead. */
.dnav-accent { color: var(--d-fg); }
.dnav-hidden { display: none; }

/* The row for the surface you are ON — the raised fill the comment above hands
   the job to.

   This rule lived in appdrawer.js's injected stylesheet, which meant it reached
   the satellites and nothing else. Three places write `gd-active` and only one
   of them loaded that stylesheet: the graph app's static rail (index.html) and
   the server-rendered first-paint skeleton (main._railed_page_shell) both load
   ONLY this file. So /graph highlighted nothing at all, and a railed page drew
   its active row flat until appdrawer.js landed and restyled it. Shared chrome
   belongs in the shared stylesheet, for the same reason .gd-mark moved here.

   --d-line2 is the injected drawer's second line colour; the static rails do
   not define it, so it falls back the way .dnav-switch already does. */
.dnav.gd-active { background: var(--d-raised); border-color: var(--d-line2, var(--d-line)); }
.dnav.gd-active .dnav-ic { color: var(--d-accent); opacity: 1; }

/* Apps this caller cannot open (see lockDrawerApps in app.js, lockApps in
   appdrawer.js, and _lock_static_rail in main.py — all three produce this
   markup). The row keeps its icon and label, because knowing Grafy HAS a Film
   Studio is why it stays visible, but it is NOT a link: those three strip the
   href, so there is no URL to preview on hover and nothing to click. The
   little pill on the right is the "off" switch: knob parked at the left,
   nothing lit. The row is dimmed, but its padlock is red so the reason reads
   at a glance.

   The cursor says the same thing the missing href does. Without this the row
   would inherit .dnav's `cursor: pointer` and keep promising a click that no
   longer happens; `default` is the honest one — `not-allowed` reads as an
   error, and being off your plan is not an error. */
.dnav-locked { opacity: .62; cursor: default; }
.dnav-locked:hover { opacity: .85; }
.dnav-switch {
  flex: none; margin-inline-start: auto; width: 24px; height: 13px; border-radius: 7px;
  border: 1px solid var(--d-line2, var(--d-line)); background: var(--d-raised); position: relative;
}
.dnav-switch::after {
  content: ""; position: absolute; top: 1px; inset-inline-start: 1.5px; width: 9px; height: 9px;
  border-radius: 50%; background: var(--d-muted);
}
/* No room for the switch on a 60px icon rail, so a padlock badge rides the
   icon instead — that one stays legible at every width. */
.drawer-collapsed .dnav-switch { display: none; }
.dnav-locked .dnav-ic { position: relative; }
.dnav-lock-badge {
  position: absolute; inset-inline-end: -5px; bottom: -4px; width: 11px; height: 11px;
  color: var(--d-lock); background: var(--d-bg); border-radius: 3px;
}
.dnav-lock-badge svg { width: 11px; height: 11px; display: block; stroke-width: 2.6; }

.dnav-ic {
  width: 18px; height: 18px; display: inline-grid; place-items: center; flex: none;
  opacity: .85; font-size: 14px;
  transition: width .2s ease, height .2s ease, font-size .2s ease, transform .2s ease;
}
.dnav-ic svg {
  width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round;
  transition: width .2s ease, height .2s ease;
}
.appdrawer-bottom .dnav-ic { color: var(--d-accent); opacity: 1; }
.dnav-accent .dnav-ic { color: inherit; opacity: .85; }

.dnav-tx { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.dnav-group { padding: 12px 11px 4px; font-size: 10.5px; letter-spacing: .08em; text-transform: uppercase; color: var(--d-muted); }
.dnav-group-list { display: none; flex-direction: column; gap: 2px; padding-inline-start: 12px; }
.graph-open .dnav-group-list { display: flex; }
.dnav-group-list .dnav { padding-top: 7px; padding-bottom: 7px; font-size: 12.5px; }
.dmore { display: none; flex-direction: column; gap: 2px; }
.more-open .dmore { display: flex; }
.dnav-caret { margin-inline-start: auto; opacity: .7; transition: transform .18s; font-size: 12px; }
.graph-open .dnav-group-toggle .dnav-caret { transform: rotate(180deg); }
.more-open .dnav-more .dnav-caret { transform: rotate(180deg); }

/* --- App switcher ---------------------------------------------------------
   Grafy is eighteen surfaces, and a rail that listed every one of them was a
   scroll rather than a menu. The 3x3 button in the drawer header opens the
   whole catalogue as a grid (appswitcher.js) and the rail keeps what you pin.
   Nothing is unpinned by default, so a rail nobody has curated is the rail it
   always was.

   The row is HIDDEN, not removed: its href still works, lockApps still sees
   it, and pinning it back costs a class. Specificity is deliberate — it has to
   beat `.more-open .dmore` and `.graph-open .dnav-group-list`, which set
   `display:flex` on the containers this also hides. */
.appdrawer .dnav.dnav-unpinned,
.appdrawer .dnav-unpinned { display: none; }

.gapps-btn {
  width: 30px; height: 30px; display: grid; place-items: center; padding: 0; flex: none;
  border: 1px solid transparent; border-radius: 8px; background: transparent;
  color: var(--d-muted); cursor: pointer;
}
.gapps-btn svg { width: 18px; height: 18px; fill: currentColor; stroke: none; }
.gapps-btn:hover { border-color: var(--d-line); color: var(--d-fg); }
/* The one header control that survives the 60px rail: it is how you get the
   other apps back, so it cannot be the thing collapsing hides. */
.drawer-collapsed .gapps-btn { display: grid; }

/* The modal lives on <body>, outside .appdrawer, so it cannot inherit the
   --d-* tokens and declares its own against the same fallbacks. */
.gapps-backdrop {
  --g-fg: var(--drawer-fg, var(--text, #f4f2ed));
  --g-muted: var(--drawer-muted, var(--muted, #979ba4));
  --g-line: var(--drawer-line, var(--line, #2b313e));
  --g-raised: var(--drawer-raised, var(--surface-raised, #1b1f2a));
  --g-panel: var(--drawer-bg, var(--surface, #12151d));
  --g-lock: var(--drawer-lock, var(--danger, #ff6b6b));
  display: none; position: fixed; inset: 0; z-index: 9300;
  background: rgba(0, 0, 0, .55);
  padding: 24px 16px;
  overflow: auto;
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
.gapps-backdrop.open { display: flex; align-items: flex-start; justify-content: center; }
.gapps-dialog {
  width: min(680px, 100%); margin-top: 6vh; padding: 18px 18px 20px;
  border: 1px solid var(--g-line); border-radius: 18px;
  background: var(--g-panel); color: var(--g-fg);
  box-shadow: 0 30px 90px rgba(0, 0, 0, .5);
}
.gapps-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.gapps-title { margin: 0; font-size: 17px; font-weight: 750; }
.gapps-close {
  width: 32px; height: 32px; display: grid; place-items: center; padding: 0; flex: none;
  border: 1px solid transparent; border-radius: 9px; background: transparent;
  color: var(--g-muted); cursor: pointer;
}
.gapps-close svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; }
.gapps-close:hover { border-color: var(--g-line); color: var(--g-fg); }
.gapps-hint { margin: 6px 0 14px; font-size: 12.5px; color: var(--g-muted); }
.gapps-grid {
  display: grid; gap: 8px;
  grid-template-columns: repeat(auto-fill, minmax(104px, 1fr));
}
.gapps-tile {
  position: relative; display: flex; flex-direction: column; align-items: center;
  gap: 8px; padding: 14px 8px 12px; border: 1px solid var(--g-line);
  border-radius: 14px; background: transparent; color: var(--g-fg);
  font: 600 12px/1.3 inherit; text-align: center; cursor: pointer;
  opacity: .52;
}
.gapps-tile:hover { background: var(--g-raised); }
/* Pinned reads as "on the rail": full strength, and a dot in the corner. */
.gapps-tile.gapps-on { opacity: 1; }
.gapps-mark {
  position: absolute; top: 8px; inset-inline-end: 8px; width: 8px; height: 8px;
  border-radius: 50%; border: 1px solid var(--g-line); background: transparent;
}
.gapps-tile.gapps-on .gapps-mark { border-color: transparent; background: var(--g-fg); }
.gapps-ic { position: relative; width: 26px; height: 26px; display: grid; place-items: center; }
.gapps-ic svg { width: 24px; height: 24px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
.gapps-ic .dnav-lock-badge { inset-inline-end: -6px; bottom: -5px; color: var(--g-lock); background: var(--g-panel); }
.gapps-tx { display: block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gapps-open {
  position: absolute; inset-inline-start: 8px; top: 8px; width: 20px; height: 20px;
  display: grid; place-items: center; border-radius: 6px;
  color: var(--g-muted); opacity: 0; transition: opacity .14s ease;
}
.gapps-open svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.gapps-tile:hover .gapps-open, .gapps-open:focus-visible { opacity: 1; }
.gapps-open:hover { color: var(--g-fg); background: var(--g-raised); }

/* --- Collapsed icon rail --------------------------------------------------
   Labels, carets, and group headers fall away; each icon grows so it stays
   legible as the whole nav item, and rows centre. The caret hides because a
   group toggle only means something beside its label — and its margin-inline-start:auto
   would otherwise claim the row's free space and push that icon off-centre. */
.drawer-collapsed .appdrawer-brand-name,
.drawer-collapsed .appdrawer-brand-app,
.drawer-collapsed .dnav-tx,
.drawer-collapsed .dnav-caret,
.drawer-collapsed .dnav-group,
.drawer-collapsed .dnav-group-list,
.drawer-collapsed .dmore,
.drawer-collapsed .dnav-more,
.drawer-collapsed .appdrawer-social,
.drawer-collapsed .appdrawer-legal { display: none; }
.drawer-collapsed .appdrawer-top { flex-direction: column; justify-content: center; gap: 8px; }
.drawer-collapsed .appdrawer-collapse { display: none; }
.drawer-collapsed:not(.drawer-overlay) .dnav { justify-content: center; padding: 8px 4px; }
.drawer-collapsed:not(.drawer-overlay) .dnav-ic { width: 24px; height: 24px; font-size: 20px; }
.drawer-collapsed:not(.drawer-overlay) .dnav-ic svg { width: 24px; height: 24px; }

.appdrawer-expand { display: none; }
.drawer-collapsed .appdrawer-expand {
  display: grid; place-items: center; width: 34px; height: 34px; padding: 0;
  border: 0; border-radius: 8px; background: transparent; color: var(--d-muted); cursor: pointer;
}
.appdrawer-expand svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.8; }
.drawer-collapsed .appdrawer-expand:hover { color: var(--d-fg); }

.appdrawer-social { display: flex; gap: 6px; padding: 8px 1px 2px; }
.appdrawer-social a {
  flex: 1; text-align: center; padding: 6px 0; border: 1px solid var(--d-line);
  border-radius: 8px; color: var(--d-muted); text-decoration: none; font-size: 11px; font-weight: 700;
}
.appdrawer-social a:hover { border-color: var(--d-accent); color: var(--d-fg); }
.appdrawer-social a svg { width: 17px; height: 17px; fill: currentColor; }

/* --- Tiny legal links -----------------------------------------------------
   A footnote row under the social icons. Sizing is min(cap, container units):
   the pair shrinks with a narrow drawer so the two labels keep sharing one
   line, but stops growing at the cap so a wide panel doesn't turn footnotes
   into another nav row. */
.appdrawer-legal {
  container-type: inline-size;
  display: flex; align-items: baseline; justify-content: center;
  gap: min(6px, 2.4cqi); padding: 6px 1px 2px;
}
.appdrawer-legal a, .appdrawer-legal .appdrawer-legal-sep {
  font-size: min(10px, 4.8cqi); line-height: 1.4;
}
.appdrawer-legal a {
  min-width: 0; color: var(--d-muted); text-decoration: none;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.appdrawer-legal a:hover { color: var(--d-fg); text-decoration: underline; }
.appdrawer-legal .appdrawer-legal-sep { color: var(--d-line); flex: none; }

/* --- Session rows ---------------------------------------------------------
   Shared by /app, /chat, and /creator. Each host supplies its own thumbnail,
   metadata, and colour variables, but the interaction stays identical:
   compact/ellipsized at rest; raised, fully wrapped, and action-enabled while
   hovered or keyboard-focused. The expanded row is absolutely positioned
   inside a fixed-height slot so it overlays its neighbours instead of moving
   the whole list. */
.grafy-session-list {
  --grafy-session-row-height: 44px;
  --grafy-session-row-gap: 4px;
  --grafy-session-row-bg: var(--panel, var(--bg-elev, #13161f));
  --grafy-session-row-hover-bg: var(--panel-2, var(--bg-elev-2, #191d28));
  --grafy-session-row-border: var(--border, var(--line, #2b313e));
  --grafy-session-row-hover-border: var(--accent, #aeb7c2);
  --grafy-session-row-text: var(--text, #f4f2ed);
  --grafy-session-row-muted: var(--muted, var(--text-dim, #979ba4));
  --grafy-session-actions-width: 106px;
  isolation: isolate;
}

.grafy-session-slot {
  position: relative;
  min-width: 0;
  min-height: var(--grafy-session-row-height);
  margin-bottom: var(--grafy-session-row-gap);
}

.grafy-session-slot:has(.grafy-session-row:hover),
.grafy-session-slot:has(.grafy-session-row:focus-within) {
  z-index: 30;
}

.grafy-session-row {
  position: absolute;
  z-index: 0;
  top: 0;
  right: 0;
  left: 0;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  min-width: 0;
  min-height: var(--grafy-session-row-height);
  overflow: hidden;
  border: 1px solid transparent;
  border-radius: 11px;
  color: var(--grafy-session-row-text);
  background: var(--grafy-session-row-bg);
  transition:
    background-color .14s ease,
    border-color .14s ease,
    box-shadow .14s ease;
}

.grafy-session-row:hover,
.grafy-session-row:focus-within {
  z-index: 30;
  overflow: visible;
  border-color: var(--grafy-session-row-hover-border);
  background: var(--grafy-session-row-hover-bg);
  box-shadow: 0 14px 28px rgba(0, 0, 0, .34);
}

/* Near the bottom of a scroller, grow upward so the last long title does not
   get clipped below the panel. A one-row list stays top-anchored. */
.grafy-session-slot:nth-last-child(-n+2):not(:first-child)
  > .grafy-session-row:is(:hover, :focus-within) {
  top: auto;
  bottom: 0;
}

.grafy-session-content {
  box-sizing: border-box;
  flex: 1 1 auto;
  min-width: 0;
}

.grafy-session-title {
  display: block;
  min-width: 0;
  max-width: 100%;
  overflow: hidden;
  color: inherit;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.grafy-session-row:is(:hover, :focus-within) .grafy-session-title {
  overflow: visible;
  text-overflow: clip;
  white-space: normal;
  overflow-wrap: anywhere;
  word-break: break-word;
}

.grafy-session-row:is(:hover, :focus-within):has(.grafy-session-actions)
  .grafy-session-content {
  padding-inline-end: var(--grafy-session-actions-width);
}

.grafy-session-actions {
  position: absolute;
  z-index: 2;
  top: 6px;
  inset-inline-end: 6px;
  display: flex;
  align-items: center;
  gap: 2px;
  box-sizing: border-box;
  min-width: calc(var(--grafy-session-actions-width) - 12px);
  padding: 2px;
  border: 1px solid color-mix(in srgb, var(--grafy-session-row-border) 75%, transparent);
  border-radius: 9px;
  background: color-mix(in srgb, var(--grafy-session-row-hover-bg) 94%, transparent);
  box-shadow: -12px 0 18px var(--grafy-session-row-hover-bg);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translateY(-2px);
  transition: opacity .12s ease, transform .12s ease, visibility 0s linear .12s;
}

.grafy-session-row:hover .grafy-session-actions,
.grafy-session-row:focus-within .grafy-session-actions {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transform: none;
  transition-delay: 0s;
}

.grafy-session-action {
  flex: 0 0 28px;
  display: grid;
  place-items: center;
  box-sizing: border-box;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 0;
  border-radius: 7px;
  color: var(--grafy-session-row-muted);
  background: transparent;
  cursor: pointer;
}

.grafy-session-action:hover,
.grafy-session-action:focus-visible {
  outline: none;
  color: var(--grafy-session-row-text);
  background: color-mix(in srgb, var(--grafy-session-row-border) 72%, transparent);
}

.grafy-session-action[data-action="delete"]:hover,
.grafy-session-action[data-action="delete"]:focus-visible {
  color: var(--danger, #ff6b6b);
  background: color-mix(in srgb, var(--danger, #ff6b6b) 12%, transparent);
}

.grafy-session-action:disabled {
  cursor: wait;
  opacity: .48;
}

.grafy-session-action svg {
  width: 15px;
  height: 15px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.grafy-session-rename {
  box-sizing: border-box;
  width: 100%;
  min-width: 0;
  padding: 6px 8px;
  border: 1px solid var(--grafy-session-row-hover-border);
  border-radius: 7px;
  outline: none;
  color: var(--grafy-session-row-text);
  background: var(--grafy-session-row-bg);
  font: inherit;
}

@media (prefers-reduced-motion: reduce) {
  .grafy-session-row,
  .grafy-session-actions { transition: none; }
}

/* NO cross-document view transitions here, and this is deliberate.

   They were tried across three attempts and every version made the thing they
   were meant to fix worse, ending in a measurement that settled it: clicking
   between surfaces with `@view-transition { navigation: auto }` painted a
   COMPLETELY BLANK frame on roughly half of all navigations. Screencasting
   every compositor frame of the same hop, six runs each:

       navigation: auto   ->  3 of 6 hops had a fully blank frame
       navigation: none   ->  0 of 6, and a third of the frames overall

   That blank frame is the "it looks like the whole page refreshed" report.
   The cause is structural: a cross-document transition suspends rendering to
   snapshot the old document and wait for the new one, which DEFEATS Chrome's
   paint holding. Paint holding is what we actually want here -- it keeps the
   previous page fully on screen until the next one can paint, so the rail
   appears untouched and only the content area changes. A view transition
   replaces that guarantee with a window in which nothing is painted at all.

   So the smooth hop comes from: paint holding (free, as long as we do not
   opt out of it), the prerender in index.html / appdrawer.js, and the
   server-rendered rail that makes the incoming page's first paint already
   correct. No transition API is involved, and none should be re-added
   without re-running that frame capture -- `navigation: auto` alone is
   enough to bring the blank frame back. */

/* Navigation feedback — the fallback for what prerender + view transitions
   don't cover (Firefox, cold first visits): the clicked rail item presses
   immediately, and if the next page still hasn't taken over after ~300ms a
   thin indeterminate bar runs along the top of the viewport. Wired twice, as
   ever: app.js for the graph app's static rail, appdrawer.js for the
   satellites — both share these styles. Fast (prerendered) hops never see
   the bar; only genuinely slow ones stop feeling broken. */
.dnav.gd-nav-pending { background: var(--d-raised); border-color: var(--d-line); }
.dnav.gd-nav-pending .dnav-ic { animation: gd-nav-pulse 0.9s ease-in-out infinite; }
@keyframes gd-nav-pulse { 50% { opacity: 0.3; } }

#gdNavProgress {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 2px;
  z-index: 9999;
  overflow: hidden;
  pointer-events: none;
  background: transparent;
}
#gdNavProgress::before {
  content: "";
  display: block;
  height: 100%;
  width: 40%;
  border-radius: 2px;
  background: var(--accent, #aeb7c2);
  animation: gd-nav-progress 1.1s ease-in-out infinite;
}
@keyframes gd-nav-progress {
  from { transform: translateX(-100%); }
  to { transform: translateX(350%); }
}
/* A transform is geometric, not logical, so the bar sweeps the same way in
   both directions unless it is told otherwise — and a progress indicator
   running against the reading direction reads as "going backwards". */
[dir="rtl"] #gdNavProgress::before { animation-name: gd-nav-progress-rtl; }
@keyframes gd-nav-progress-rtl {
  from { transform: translateX(100%); }
  to { transform: translateX(-350%); }
}
@media (prefers-reduced-motion: reduce) {
  .dnav.gd-nav-pending .dnav-ic { animation: none; opacity: 0.5; }
  #gdNavProgress::before { animation: none; width: 100%; opacity: 0.5; }
}
