/* ============================================================
   ui.css — Shared component styles built on Open Props.
   This is the single stylesheet consumers link to; it pulls in
   the Open Props token layer, selected extras and the form
   styles in forms.css via @import. Styles that only one
   component or decorator uses live in that module instead,
   which injects them ahead of this sheet.
   @tt-about ui-style-delivery
   ============================================================ */

@import url("../open-props/index.css");
@import url("../open-props/extra/utilities.css");
@import url("forms.css");

/* Visually hidden content.
@tt-about screen-reader-announcements
Override the imported zero-size utility with a clipped 1px box and white-space: nowrap. The zero-size version wraps each word separately and can cause screen readers to lose the word breaks. Keep this after @import so these physical dimensions override the vendor’s logical dimensions at equal specificity. Do not edit public/open-props/. accordion() and tree() inject the same recipe for their live regions. Remove the override when Open Props supplies it. See /ui/docs/styles-utilities.html. */
:is(.visually-hidden, .sr-only) {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* From open-props/extra/normalize.src.css. Every width, height and
   aspect-ratio value in this sheet therefore already includes padding and
   border — don't re-declare box-sizing per component, and don't read a
   dimension anywhere in the layer as content-box. */
*, *::before, *::after {
  box-sizing: border-box;
}

[hidden] {
  display: none !important;
}

/* ---- Reading font ---- */

.font-reading {
  font-family: Literata, Georgia, serif;
}

/* ---- Buttons — see ui/docs/ui-button.html ---- */

.btn {
  display: inline-flex;
  align-items: center;
  gap: var(--size-2);
  border: none;
  border-radius: var(--radius-2);
  font-size: var(--font-size-1);
  font-weight: var(--font-weight-5);
  line-height: normal;
  cursor: pointer;
  padding: var(--size-2) var(--size-3);
  background: var(--brand);
  color: var(--on-brand, white);
  text-decoration: none;

  &:hover {
    box-shadow: var(--shadow-2);
  }

  &.ghost {
    background: transparent;
    color: var(--text-2);

    &:hover {
      background: var(--surface-3);
      color: var(--text-1);
      box-shadow: none;
    }
  }

  &.outline {
    background: transparent;
    color: var(--text-1);
    border: var(--border-size-1) solid var(--border-color);

    &:hover {
      background: var(--surface-3);
      box-shadow: none;
    }
  }

  &.destructive {
    /* Deep fill + white ink works on both color schemes — scheme-constant
       on purpose (--error is the text-role token and flips too light). */
    background: var(--red-9);
    color: white;

    &:hover {
      background: var(--red-10);
    }
  }

  &.full-width {
    width: 100%;
    justify-content: center;
  }

  &.sm {
    font-size: var(--font-size-0);
    padding: var(--size-1) var(--size-2);
  }

  &[aria-pressed="true"] {
    background: var(--surface-3);
  }
}

.btn-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--size-7);
  height: var(--size-7);
  padding: 0;
  border: none;
  border-radius: var(--radius-2);
  cursor: pointer;
  background: transparent;
  color: var(--text-1);

  &:hover {
    background: var(--surface-3);
  }

  /* Icon-only buttons get a chunkier glyph so the 32px button doesn't feel
     mostly empty (default 1.1em → ~55% fill; 1.4em → ~75% fill, matching
     iOS/Material toolbar proportions). Icons in `.btn` next to text labels
     keep the slimmer default. */
  & > ui-icon {
    font-size: 1.4em;
  }
}

/* ---- Badge ---- */

.badge {
  --_bg: var(--brand);
  --_color: var(--on-brand, white);

  display: inline-flex;
  align-items: center;
  gap: var(--size-1);
  background: var(--_bg);
  color: var(--_color);
  font-size: var(--font-size-0);
  font-weight: var(--font-weight-6);
  padding: var(--size-1) var(--size-2);
  border-radius: var(--radius-round);
  border: var(--border-size-1) solid transparent;

  &.secondary {
    --_bg: var(--surface-3);
    --_color: var(--text-1);
  }

  &.outline {
    --_bg: transparent;
    --_color: var(--text-1);

    border: var(--border-size-1) solid var(--border-strong);
  }

  &.brand {
    /* Default is already brand-colored; explicit class for clarity */
  }

  /* Use deep fills with white text in both themes. The destructive variant shares
the same red fill with buttons, whether describing an action or an outcome.
See /ui/docs/styles-modifiers.html. */
  &.destructive {
    --_bg: var(--red-9);
    --_color: white;
  }

  &.warning {
    --_bg: var(--orange-9);
    --_color: white;
  }

  &.cancelled {
    --_bg: var(--stone-5);
    --_color: white;
  }

  /* Opacity fade for "live/active" badges (e.g. "Recording in progress"). Open
   * Props' `--animation-pulse` is a scale bounce, not a fade, so the fade is
   * defined here. `prefers-reduced-motion` opt-out. */
  &.pulse {
    animation: badge-pulse 2s var(--ease-in-out-3, ease-in-out) infinite;
  }
}

@keyframes badge-pulse {
  50% { opacity: 0.55; }
}

@media (prefers-reduced-motion: reduce) {
  .badge.pulse { animation: none; }
}

/* ---- Callout (tinted remark about something else) — see ui/docs/ui-callout.html ----
 * Shape-only primitive; callers set `--_tint` on the element (or via a
 * variant class), because the tint carries the meaning. `.bordered` adds a
 * matching outline and space-between action layout. `.callout-actions` is the
 * inline action cluster on the right edge of a bordered callout; controls are
 * optional, and a callout may hold only an icon and a sentence.
 */

.callout {
  display: flex;
  align-items: center;
  gap: var(--size-2);
  padding: var(--size-2) var(--size-3);
  border-radius: var(--radius-2);
  font-size: var(--font-size-0);
  background: color-mix(in srgb, var(--_tint, var(--text-2)) 12%, transparent);

  &.bordered {
    border: var(--border-size-1) solid color-mix(in srgb, var(--_tint, var(--text-2)) 28%, transparent);
    justify-content: space-between;
  }
}

.callout-actions {
  display: inline-flex;
  gap: var(--size-2);
}

/* ---- Disclosure / Accordion -------------------------------------------
 * Styled native <details>/<summary>. Presentation is pure CSS and works with
 * ZERO JavaScript — hand-write a single <details class="ui-disclosure"> or an
 * exclusive group of <details name="…"> inside <div class="ui-accordion">
 * (the MDN/APG exclusive-accordion `name` pattern). The accordion() decorator
 * (public/ui/lib/accordion.mjs) layers on the repetitive APG wiring (panel
 * role="region" + aria-labelledby), exclusivity and lazy `load()` — see
 * public/ui/docs/ui-accordion.html.
 *
 * Summary layout: chevron · label · (optional) <span class="ui-disclosure-aside">
 * trailing meta/badge. Panel content is wrapped in .ui-disclosure-panel (the
 * decorator creates it; hand-written markup writes it).
 */

.ui-accordion {
  display: flex;
  flex-direction: column;
  gap: var(--size-2);
}

.ui-disclosure,
.ui-accordion > details {
  border: var(--border-size-1) solid var(--border-color);
  border-radius: var(--radius-2);
  background: var(--surface-1);
}

.ui-disclosure > summary,
.ui-accordion > details > summary {
  display: flex;
  align-items: center;
  gap: var(--size-2);
  padding: var(--size-2) var(--size-3);
  border-radius: inherit;
  cursor: pointer;
  user-select: none;
  font-weight: var(--font-weight-6);
  color: var(--text-1);
  list-style: none; /* hide the native disclosure marker (standards) */
}
/* hide the native marker (WebKit) */
.ui-disclosure > summary::-webkit-details-marker,
.ui-accordion > details > summary::-webkit-details-marker { display: none; }

.ui-disclosure > summary:hover { background: var(--surface-3); }
.ui-disclosure > summary:focus-visible {
  outline: var(--border-size-2) solid var(--brand);
  outline-offset: -2px;
}
.ui-accordion > details > summary:hover { background: var(--surface-3); }
.ui-accordion > details > summary:focus-visible {
  outline: var(--border-size-2) solid var(--brand);
  outline-offset: -2px;
}

/* Leading chevron — pure CSS, rotates when open. Decorative (the open/closed
   state is conveyed natively by <summary>), so it never leaks into the name. */
.ui-disclosure > summary::before,
.ui-accordion > details > summary::before {
  content: "";
  flex: none;
  width: 0;
  height: 0;
  border-inline-start: 0.4em solid currentColor;
  border-block: 0.32em solid transparent;
  opacity: 0.55;
  transition: transform 0.15s var(--ease-2);
}
.ui-disclosure[open] > summary::before,
.ui-accordion > details[open] > summary::before { transform: rotate(90deg); }

/* Trailing cluster pinned to the inline-end of the summary row. */
.ui-disclosure-aside {
  margin-inline-start: auto;
  display: inline-flex;
  align-items: center;
  gap: var(--size-2);
  font-weight: var(--font-weight-4);
}

/* The disclosed region. */
.ui-disclosure-panel {
  padding: var(--size-2) var(--size-3) var(--size-3);
  color: var(--text-1);
}

/* Open/close height animation — progressive enhancement (Chrome/Edge 131+ via
   ::details-content + interpolate-size); every other engine opens instantly,
   which is the native default. */
@supports selector(::details-content) {
  .ui-disclosure,
  .ui-accordion { interpolate-size: allow-keywords; }
  .ui-disclosure::details-content,
  .ui-accordion > details::details-content {
    block-size: 0;
    overflow: clip;
    /* Transition content-visibility too (allow-discrete) so the CLOSE animates
       at the same 0.2s as the open — otherwise a panel the browser auto-closes
       (exclusive `name` group, or a second click) snaps shut while its sibling
       slides open, which reads as broken. */
    transition: block-size 0.2s var(--ease-2), content-visibility 0.2s var(--ease-2);
    transition-behavior: allow-discrete;
  }
  .ui-disclosure[open]::details-content,
  .ui-accordion > details[open]::details-content { block-size: auto; }
}

@media (prefers-reduced-motion: reduce) {
  .ui-disclosure > summary::before,
  .ui-accordion > details > summary::before { transition: none; }
  .ui-disclosure::details-content,
  .ui-accordion > details::details-content { transition: none; }
}

/* ---- Focus ---- */

input:focus-visible, textarea:focus-visible, select:focus-visible,
.btn:focus-visible, .btn-icon:focus-visible {
  outline: var(--border-size-2) solid var(--brand);
  outline-offset: var(--border-size-2);
}

/* ---- Cards ---- */

.card-link {
  grid-column: 1;
  grid-row: 1 / span 2;
  display: grid;
  gap: var(--size-1);
  color: inherit;
  text-decoration: none;
  border-radius: var(--radius-2);
  /* Allows child text to truncate inside a flex/grid parent. */
  min-width: 0;

  &:visited { color: inherit; }

  &:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: 2px;
  }
}

.card-title,
.card-meta {
  grid-column: 1;
  display: flex;
  align-items: center;
}

.card-title {
  gap: var(--size-2);
  /* Title + badge(s) wrap on narrow screens. */
  flex-wrap: wrap;

  & h4 {
    font-size: var(--font-size-1);
    font-weight: var(--font-weight-5);
    margin: 0;
    transition: color .2s var(--ease-2);
    /* Truncate long titles with ellipsis.
       min-width:0 lets the h4 shrink below its content width inside the flex
       .card-title row — without it (flex default min-width:auto) the nowrap title
       never shrinks and the ellipsis can't fire. */
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}

.card-meta {
  flex-wrap: wrap;
  gap: var(--size-1) var(--size-3);

  & small {
    display: inline-flex;
    align-items: center;
    gap: var(--size-1);
    font-size: var(--font-size-0);
    color: var(--text-2);
  }

  & .committed-date { color: var(--brand); }
}

article {
  background: var(--surface-2);
  border: var(--border-size-1) solid var(--border-color);
  border-radius: var(--radius-3);
  padding: var(--size-4) var(--size-5);
  transition: border-color .2s var(--ease-2);
  margin-bottom: var(--size-2);
  display: grid;
  grid-template-columns: 1fr auto;
  gap: var(--size-1) var(--size-3);
  align-items: start;

  & menu {
    grid-column: 2;
    grid-row: 1 / -1;
    align-self: center;
    margin: 0;
    padding: 0;
    list-style: none;
    /* Place the action slot above the stretched card-link target so its buttons
remain clickable. Grid items accept z-index without position.
See /ui/docs/ui-card.html § Accessibility. */
    z-index: 1;
  }

  /* Add a media column only when card-media exists; other cards retain the
two-column grid. The auto track uses the image’s width. Applications can
render a placeholder tile to align titles when only some cards have images.

Keep this rule between the menu and hover rules. That order allows its override
while satisfying no-descending-specificity. */
  &:has(> .card-media) {
    grid-template-columns: auto 1fr auto;

    & > .card-link { grid-column: 2; }
    & > menu { grid-column: 3; }
  }

  /* Create a containing block only for cards with a stretched link. Plain article
content must not unexpectedly become the positioning reference for descendants. */
  &:has(> .card-link) {
    position: relative;
  }

  /* Stretch the inner anchor’s hit area across the card with a pseudo-element.
Keep the anchor around its own text: wrapping the whole card would include
unrelated text in its accessible name and nest action buttons inside a link
(/ui/docs/ui-card.html § Accessibility). Clicks on the pseudo-element target
the anchor, preserving navigation and delegated event handling.

Inset zero covers the card’s padding box, including gaps and the media tile.
The image remains outside the anchor markup to keep it out of the accessible
name while still being clickable.

Create the overlay only for a link with a destination. Unavailable cards have
no href or role and explain their aria-disabled state in a title; a full-card
hover effect would incorrectly suggest they could be opened. */
  & > .card-link:is([href], [role="button"])::after {
    content: "";
    position: absolute;
    inset: 0;
  }

  &:has(> .card-link:hover, > .card-link:focus-visible) {
    border-color: var(--border-strong);

    & h4 {
      color: var(--brand);
    }
  }

  /* Draw the focus ring around the full card because that is the link’s hit area
(WCAG 2.4.11). Remove the anchor’s smaller ring only inside cards. Unstretched
card-link elements elsewhere, such as side-panel rows, keep their own rings. */
  & > .card-link:focus-visible {
    outline: none;
  }

  &:has(> .card-link:is([href], [role="button"]):focus-visible) {
    outline: var(--border-size-2) solid var(--brand);
    outline-offset: var(--border-size-2);
  }

  & > :not(.card-media, .card-link, menu) {
    grid-column: 1 / -1;
    /* Above the overlay, same reason as `& menu`: an extra row is its own content — a
       disclosure panel's rows carry their own links — not part of the card's click target. */
    z-index: 1;
  }
}

/* The picture that identifies a card's subject, beside the body rather than above it.
   `1 / span 2` is `.card-link`'s own span, so the two stand side by side and a third row —
   a progress strip, a disclosure panel — still starts underneath both. */
.card-media {
  grid-column: 1;
  grid-row: 1 / span 2;
  display: grid;
  place-items: center;
  overflow: hidden;
  inline-size: var(--card-media-size, 4rem);
  aspect-ratio: 2 / 3;
  background: var(--surface-3);
  border-radius: var(--radius-2);
  /* Apply the resting shadow to the media tile. Its img fills an overflow-clipped
box, so an image-level shadow would disappear. Shadow-2 fits the documented
resting range and remains visible at card-media-size; shadow-1 resembles only
a bottom hairline. Keep shadow strength controlled by the app-wide token
(/ui/docs/styles-elevation.html). */
  box-shadow: var(--shadow-2);
  color: var(--text-2);

  /* Size the placeholder icon relative to the tile. Inheriting text size would
make it too small to clearly identify a missing picture. */
  & > ui-icon { font-size: var(--font-size-5); }

  /* Use contain to preserve the complete cover artwork and title. The tile’s
aspect ratio may differ from the image’s, so allow letterboxing instead of cropping. */
  & img {
    inline-size: 100%;
    block-size: 100%;
    object-fit: contain;
  }
}

article > menu .chevron {
  transition: transform .2s var(--ease-2);

  &[aria-expanded="true"] { transform: rotate(90deg); }
}

/* Responsive card adjustments:
   - Tighter padding on mobile.
   - Larger touch targets for icon buttons in card menus. */
@media (max-width: 640px) {
  article { padding: var(--size-3) var(--size-4); }
  article > menu .btn-icon { width: 2.5rem; height: 2.5rem; }
  /* The narrower the card, the more of it the picture takes; the title needs the room more. */
  .card-media { --card-media-size: 3rem; }
}

/* ---- Empty state ---- */

.empty-state {
  text-align: center;
  padding: var(--size-8) 0;
  color: var(--text-2);

  /* Keep this host inline-level so text-align can center its glyph. Setting
display:block would center the box while leaving the glyph misaligned.
See /ui/docs/ui-status.html § Empty state. */
  & > ui-icon {
    font-size: var(--font-size-5);
    margin-block-end: var(--size-2);
  }

  & > p { margin: 0 }

  /* Wherever this markup appears, a second paragraph is the subtitle under the title. */
  & > p ~ p {
    margin-block-start: var(--size-1);
    font-size: var(--font-size-0);
  }
}

/* ---- Interactive states ---- */

.btn:disabled,
.btn-icon:disabled {
  opacity: 0.5;
  pointer-events: none;
}

.btn[aria-disabled="true"],
.btn-icon[aria-disabled="true"] {
  opacity: 0.5;
  cursor: not-allowed;
}

/* ---- Spinner ---- */

.spin {
  animation: var(--animation-spin);
}

/* ---- Status animations ---- */

@keyframes status-flash {
  0% {
    background-color: transparent;
    transform: scale(1);
  }

  15% {
    background-color: color-mix(in oklch, var(--success, seagreen) 25%, transparent);
    transform: scale(1.03);
  }

  100% {
    background-color: transparent;
    transform: scale(1);
  }
}

.status-success {
  color: var(--success, seagreen);
  border-radius: var(--radius-2);
  animation: status-flash 0.8s var(--ease-3);

  @media (prefers-reduced-motion: reduce) {
    animation: status-flash 1s var(--ease-3);
  }
}

.status-error {
  color: var(--error, crimson);
  animation: shake-x 0.75s var(--ease-out-5);

  @media (prefers-reduced-motion: reduce) {
    animation: none;
  }
}

/* Reveal flash identifies the element the user just selected. Tint it without changing its text color. reveal() and play_once() in /ui/lib/animation.mjs restart the animation and remove its class when it finishes. */
@keyframes reveal-flash {
  0% {
    background-color: transparent;
    transform: scale(1);
  }

  15% {
    background-color: color-mix(in oklch, var(--info, steelblue) 22%, transparent);
    transform: scale(var(--_reveal-scale));
  }

  100% {
    background-color: transparent;
    transform: scale(1);
  }
}

.reveal-flash {
  --_reveal-scale: 1.02;
  border-radius: var(--radius-2);
  animation: reveal-flash 0.8s var(--ease-3);

  /* Keep the tint for users who request reduced motion so the revealed element is still identifiable. Extend its duration for readability and remove the scale animation. */
  @media (prefers-reduced-motion: reduce) {
    --_reveal-scale: 1;
    animation-duration: 1.2s;
  }
}

/* Attention pulse makes a focus change visible after a pointer gesture, when :focus-visible may not draw a ring. Use --brand to match the normal focus color.
Animate ::after so the ring does not compete with animation or all declarations on the host, or obscure a control whose background indicates its state. Make a static host a containing block with zero-specificity :where(), preserving any author-supplied positioning.
Call play_once(el, class, {pseudo: "::after"}) from /ui/lib/animation.mjs. The pseudo option is required to read the correct computed animation duration; without it, the fallback removes the class too early. */
@keyframes attention-pulse {
  from {
    opacity: 1;
    transform: scale(1);
  }

  to {
    opacity: 0;
    transform: scale(var(--_pulse-scale));
  }
}

:where(.attention-pulse) {
  position: relative;
}

.attention-pulse::after {
  --_pulse-scale: 2.2;
  content: "";
  position: absolute;
  inset: 0;
  /* A host can give the ring a state colour of its own with --attention-pulse-color, and
start it later with --attention-pulse-delay to time it to another animation. The ring is
transparent outside its keyframes, so it does not show while it waits. */
  border: var(--border-size-2) solid var(--attention-pulse-color, var(--brand));
  border-radius: inherit;
  opacity: 0;
  /* Disable pointer interaction on the animated ring so it cannot intercept
clicks on neighboring controls while extending beyond the host. */
  pointer-events: none;
  animation: attention-pulse 0.8s var(--ease-out-3) var(--attention-pulse-delay, 0s);

  /* With reduced motion, keep the ring visible longer and remove its growth,
as with status-success and reveal-flash. At scale 1 it fades in place. */
  @media (prefers-reduced-motion: reduce) {
    --_pulse-scale: 1;
    animation-duration: 1.2s;
  }
}
