/* ALY — the workspace.
   Everything here builds on the shared ALYSSIUN tokens in style.css. No brand
   colour, font or shadow is hard-coded, and no local theme system exists: LUX
   (Ceramic Mist) is the default and ALYSSIUN OS owns the LUX/NEO switch.

   ALY is an AI workspace, not a page: the app shell is a fixed-height grid, the
   conversation is the only thing that scrolls, and the composer is always
   reachable. */

.aly-body {
  /* Keyboard-aware height from aly-viewport.js; 100dvh is the fallback. */
  height: var(--aly-viewport-height, 100dvh);
  /* style.css gives every body `min-height: 100vh`, and 100vh is the LARGE
     viewport on phones (browser bars hidden): taller than the shell whenever
     the bars or the keyboard are showing, which left page under the shell. */
  min-height: 0;
  padding-bottom: 0;
  overflow: hidden;
}

.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  background: var(--void); color: var(--text); padding: 12px 18px; border: 1px solid var(--hair-2);
}
.skip-link:focus { left: 12px; top: 12px; }

.aly-visually-hidden {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  clip-path: inset(50%); overflow: hidden; white-space: nowrap;
}

/* Focus is never removed, only styled. */
:where(button, a, textarea, input, [tabindex]):focus-visible {
  outline: 2px solid var(--text); outline-offset: 2px; border-radius: 3px;
}

/* --- Shell ---------------------------------------------------------------- */
.aly-shell {
  height: var(--aly-viewport-height, 100dvh);
  display: grid;
  grid-template-columns: 288px minmax(0, 1fr);
  min-width: 0;
  min-height: 0;
  overflow: hidden;
}
.aly-shell[hidden] { display: none; }

/* Inside a Project, the sidebar stands down and the workspace takes the width.
   Two panes want the room — ALY on the left, the running application on the
   right — and recent conversations are not what you are looking at while you
   build. The Exit control in the workspace header is the way back, and it is
   the reason this is safe to hide: navigation moved, it did not disappear.

   Below 900px the sidebar is already an off-canvas drawer that costs no layout
   width, so focus mode leaves it alone there and ☰ keeps working. */
@media (min-width: 901px) {
  .aly-shell.is-project-focus { grid-template-columns: minmax(0, 1fr); }
  .aly-shell.is-project-focus .aly-sidebar { display: none; }
}

/* --- Main column ---------------------------------------------------------- */
/* The column track is EXPLICIT for a reason. Without it the single implicit
   column is `auto`, which resolves to the max-content of its widest row — and
   the moment a conversation is open the top bar gains Rename and Delete, whose
   max-content is 521px. On a 320px phone that stretched .aly-main, and with it
   the transcript and the composer, 200px wider than the screen: the app zoomed
   out, and every fixed control landed somewhere other than where it was tapped.
   min-width:0 does not help, because the overflow is the TRACK, not the item. */
.aly-main {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: auto minmax(0, 1fr) auto;
  min-width: 0; min-height: 0;
  overflow: hidden;
}

.aly-topbar {
  display: flex; align-items: center; gap: 12px; min-height: 60px;
  padding: 0 clamp(14px, 3vw, 28px);
  border-bottom: 1px solid var(--hair);
  background: var(--nav-bg);
  -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
}
.aly-title {
  margin: 0; font-size: 15px; font-weight: 400; letter-spacing: 0.01em;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1;
}
.aly-topbar-actions { display: flex; gap: 6px; }

.aly-icon-btn {
  border: 1px solid transparent; background: none; color: var(--muted);
  font: inherit; font-size: 13px; cursor: pointer; padding: 7px 10px; border-radius: 6px;
  line-height: 1;
}
.aly-icon-btn:hover:not(:disabled) { color: var(--text); background: var(--glass-2); }
.aly-icon-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.aly-icon-btn.aly-danger:hover { color: var(--danger); }
.aly-sidebar-open, .aly-sidebar-close { display: none; font-size: 16px; }

/* The one "copy this for support" control — an error message, the DETAILS
   panel, the build console, a capability report. Built on `.aly-icon-btn`
   above, so it already reads correctly in both lux and neo without a colour
   of its own; `--jade` (ALY's one restrained "verified" tint, defined for
   both atmospheres) is the only addition, for the brief copied confirmation. */
.aly-copy-btn { padding: 5px 7px; font-size: 12px; }
.aly-copy-icon { display: inline-block; }
.aly-copy-btn.is-copied { color: var(--jade); }

/* --- Transcript ----------------------------------------------------------- */
.aly-transcript {
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  scrollbar-gutter: stable;
  touch-action: pan-y;
  padding: 0 clamp(14px, 4vw, 28px);
}
.aly-messages {
  width: min(760px, 100%); margin: 0 auto;
  padding: 26px 0 10px; display: flex; flex-direction: column; gap: 30px;
}

/* The identity area. The orb, the heading and the capability row sit directly
   on the atmosphere — no card around them; the space itself is the frame. */
.aly-identity {
  width: min(760px, 100%); margin: 0 auto;
  min-height: 52vh; min-height: 52dvh; display: flex; flex-direction: column;
  align-items: center; justify-content: center; text-align: center;
  padding: 40px 0 16px;
}
.aly-identity[hidden] { display: none; }
.aly-identity-card {
  position: relative; isolation: isolate; width: 100%;
  display: flex; flex-direction: column; align-items: center; gap: 0;
}
/* A faint jade bloom behind the orb only — light, not a surface. */
.aly-identity-card::before {
  content: ""; position: absolute; z-index: -1;
  left: 50%; top: 0; transform: translateX(-50%);
  width: clamp(200px, 40%, 320px); aspect-ratio: 1; border-radius: 50%;
  background: radial-gradient(circle, color-mix(in srgb, var(--jade) 32%, transparent) 0%, transparent 66%);
  filter: blur(36px); opacity: 0.55; pointer-events: none;
}
/* The hero position: ALY at full presence, before a conversation has filled up.
   Larger than the mark it replaces, because the sphere needs room to read. */
.aly-identity-mark {
  width: clamp(150px, 22vw, 216px); height: clamp(150px, 22vw, 216px);
  display: grid; place-items: center;
  position: relative;
}
/* The typographic fallback. Visible only where WebGL is not — a device without
   it still sees an ALY identity, never an empty hole. */
.aly-identity-mark > span {
  width: 108px; height: 108px; display: grid; place-items: center;
  border: 1px solid var(--hair-2); border-radius: 50%;
  background: var(--glass-2); box-shadow: var(--card-shadow);
  font-size: 26px; font-weight: 200; letter-spacing: 0.22em; text-indent: 0.22em; color: var(--text);
}
.aly-sphere-mount.has-sphere > span { display: none; }
.aly-sphere-mount canvas { display: block; width: 100%; height: 100%; }

/* The compact position in the topbar: ALY stays present during a long
   conversation without taking attention from it. */
.aly-sphere-compact { width: 34px; height: 34px; flex: 0 0 auto; display: none; }
.aly-sphere-compact.has-sphere { display: block; }

/* State, in words. Never colour alone, never the animation alone. */
/* nowrap + a flex item's default min-width:auto made this unshrinkable: at 320px
   the top bar needed 372px and overflowed by 80px. Still one line, but it may
   now give up its room — the title and the two actions matter more. */
.aly-status {
  margin: 0; font-size: 11px; font-weight: 600; letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--muted); white-space: nowrap;
  min-width: 0; overflow: hidden; text-overflow: ellipsis;
}
.aly-status[hidden] { display: none; }
.aly-identity-title {
  margin: 22px 0 0; font-size: clamp(34px, 5.6vw, 54px); font-weight: 200;
  letter-spacing: -0.025em; line-height: 1.08;
}

/* Messages. ALY's turns are unadorned prose — the answer is the interface. The
   person's own turns carry a light surface so the exchange stays scannable. */
/* min-width:0 on every flex child: without it a flex item refuses to shrink
   below the width of its longest unbreakable run, so one long URL pushes the
   whole transcript sideways at 390px. */
.aly-message { position: relative; display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.aly-message > * { min-width: 0; max-width: 100%; }
/* How long ALY's engine took, top-right of the reply. Quiet by default; it is
   a diagnostic a person can glance at, not a headline. Scoped to a direct
   child so it never leaks onto the workspace's own .aly-elapsed badge. */
.aly-message > .aly-elapsed {
  position: absolute; top: 0; right: 0;
  font-size: 10px; font-variant-numeric: tabular-nums; letter-spacing: 0.04em;
  color: var(--muted); opacity: 0.7; white-space: nowrap; pointer-events: none;
}
.aly-message.is-failed > .aly-elapsed { color: var(--danger); opacity: 0.85; }
/* Present, not shouting — the words are what somebody is here to read, and a
   role label repeated on every turn is the part of the transcript that reads
   as noise fastest. */
.aly-message-role {
  font-size: 10px; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--muted); opacity: 0.7;
}
.aly-message-body { font-size: 15.5px; line-height: 1.72; overflow-wrap: anywhere; min-width: 0; }
.aly-message-body > *:first-child { margin-top: 0; }
.aly-message-body > *:last-child { margin-bottom: 0; }
.aly-message-body p { margin: 0 0 14px; }
.aly-message-body ul, .aly-message-body ol { margin: 0 0 14px; padding-left: 22px; }
.aly-message-body li { margin-bottom: 5px; }
/* A bare <pre> — anything not built as a code box by aly-render-code.js. */
.aly-message-body pre {
  background: var(--void-2); border: 1px solid var(--hair); border-radius: 8px;
  padding: 14px 16px; overflow-x: auto; margin: 0 0 14px; font-size: 13.5px; line-height: 1.6;
  max-width: 100%;
}
.aly-message-body code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.92em; }
.aly-message-body :not(pre) > code { background: var(--void-2); padding: 1px 5px; border-radius: 4px; }

/* The person's own turns read as a bubble on the right, the way any messaging
   app distinguishes "you" from the other side without a shouted label doing
   the work — ALY's own turns stay plain, unindented prose either side. */
.aly-message.is-user { align-items: flex-end; }
.aly-message.is-user .aly-message-body {
  max-width: 88%;
  background: var(--glass-2); border: 1px solid var(--hair);
  border-radius: 18px; padding: 12px 16px; white-space: pre-wrap;
}
.aly-message.is-failed .aly-message-body { border-left: 2px solid var(--danger); padding-left: 13px; }

.aly-agent-card { width:min(760px,100%); border:1px solid var(--hair-2); border-radius:var(--radius); background:var(--glass-2); padding:14px 16px; }
.aly-agent-select { width:100%; margin:8px 0; padding:8px 10px; border:1px solid var(--hair-2); border-radius:var(--radius); background:var(--glass-2); color:inherit; font:inherit; }
.aly-agent-title { margin:0; font-size:14px; font-weight:600; }
.aly-agent-copy { margin:6px 0 12px; color:var(--muted); font-size:13px; line-height:1.5; }

/* Streaming cursor — a hairline, not a blinking block. */
.aly-cursor {
  display: inline-block; width: 2px; height: 1.05em; vertical-align: text-bottom;
  background: var(--text); margin-left: 2px; animation: aly-blink 1.1s steps(2, start) infinite;
}
@keyframes aly-blink { to { opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .aly-cursor { animation: none; } }
