/* ==========================================================================
   glassmorphism-theme / components
   --------------------------------------------------------------------------
   Five components: button, card, input, badge, nav.

   Every value here is a var() reference to tokens.css. There is not one color
   literal in this file, which is what makes retheming a matter of editing one
   file rather than auditing five.

   The class names follow the shadcn shape: one base class carrying the
   structure, one variant class carrying the intent (gl-btn + gl-btn-primary).
   If you are on shadcn already, these map onto its variant prop directly.

   Requires tokens.css to be loaded first. Load it explicitly rather than
   letting this file pull it in, so the dependency is visible at the call site:

     <link rel="stylesheet" href="tokens/tokens.css" />
     <link rel="stylesheet" href="components/components.css" />
     <link rel="stylesheet" href="components/depth.css" />

   The third file draws the ground these components are translucent against.
   Nothing here imports it, and nothing here breaks without it, but a frosted
   surface over a flat fill is a gray box. See depth.css.

   THREE THINGS THAT SILENTLY KILL backdrop-filter
   Worth knowing before you debug a panel that will not frost.

     1. An ancestor carrying `opacity` below 1 or a `filter` establishes a
        backdrop root the backdrop cannot reach past. The panel keeps its fill
        and quietly loses its blur. This is the usual cause. The rule is often
        quoted with `transform` in the list too; measured on Chromium, a
        transformed ancestor did not break the blur. depth.css rule 4 carries
        the numbers and the controls.
     2. There has to be something behind it. A panel over a solid fill blurs a
        solid fill and looks identical either way, which is why depth.css
        ships a veil rather than a bare gradient.
     3. Stacking two backdrop-filters means the upper one blurs the lower
        one's already-composited output, so the alphas multiply rather than
        add. depth.css states the rule; the short version is that a tier-3
        surface inside a tier-2 surface is one tier too many.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Shared surface
   The material, in one place. Three things make a translucent rectangle read
   as a pane rather than as a hole: the fill, the blur behind it, and a light
   edge along the top. The third is the one people leave out.

   The inset highlight is a single hairline at the top edge only. A pane picks
   up the light source above it, so lighting all four sides states two light
   sources and reads as a glowing rectangle instead of a physical object.
   -------------------------------------------------------------------------- */
.gl-surface {
  background: var(--gl-surface-2);
  -webkit-backdrop-filter: var(--gl-backdrop-2);
  backdrop-filter: var(--gl-backdrop-2);
  border: var(--gl-border-width) solid var(--gl-edge-boundary);
  box-shadow:
    inset 0 var(--gl-border-width) 0 0 var(--gl-highlight),
    var(--gl-shadow-2);
}

.gl-focusable:focus-visible {
  outline: var(--gl-ring-width) solid var(--gl-ring);
  outline-offset: var(--gl-ring-offset);
}

/* ==========================================================================
   BUTTON
   Variants: gl-btn-primary, gl-btn-default, gl-btn-ghost, gl-btn-destructive
   Sizes:    gl-btn-sm, (base), gl-btn-lg

   Only the default and ghost variants are glass. The two that carry an action
   are opaque, because a translucent label's contrast is a function of where
   the reader happens to have scrolled to.
   ========================================================================== */
.gl-btn {
  /* 44px minimum on both axes. Anything smaller fails on a real thumb. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--gl-space-2);
  min-height: 44px;
  padding: var(--gl-space-3) var(--gl-space-6);

  font-family: var(--gl-font-sans);
  font-size: var(--gl-text-body);
  font-weight: var(--gl-weight-bold);
  line-height: var(--gl-leading-snug);
  text-decoration: none;
  white-space: nowrap;

  border: var(--gl-border-width) solid var(--gl-edge-strong);
  border-radius: var(--gl-radius);
  cursor: pointer;

  transition:
    transform var(--gl-duration) var(--gl-ease),
    box-shadow var(--gl-duration) var(--gl-ease),
    background-color var(--gl-duration) var(--gl-ease);
}

/* The gesture: a surface rises toward the reader and its shadow grows to
   match. Pressing puts it back down. Nothing translates on click beyond
   returning to rest, so the two states stay distinguishable. */
.gl-btn:hover:not(:disabled) {
  transform: translateY(calc(var(--gl-lift) * -1));
  box-shadow: var(--gl-shadow-3);
}

.gl-btn:active:not(:disabled) {
  transform: translateY(0);
  box-shadow: var(--gl-shadow-1);
}

.gl-btn:focus-visible {
  outline: var(--gl-ring-width) solid var(--gl-ring);
  outline-offset: var(--gl-ring-offset);
}

.gl-btn:disabled,
.gl-btn[aria-disabled="true"] {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
  box-shadow: none;
}

.gl-btn-primary {
  background: var(--gl-primary);
  color: var(--gl-primary-ink);
  box-shadow: var(--gl-shadow-2);
}

.gl-btn-destructive {
  background: var(--gl-destructive);
  color: var(--gl-destructive-ink);
  box-shadow: var(--gl-shadow-2);
}

/* The glass button. Its fill is a tier-2 surface, so it belongs on the ground
   or on a tier-1 well. Put one on a tier-2 card and the two alphas stack. */
.gl-btn-default {
  background: var(--gl-surface-2);
  -webkit-backdrop-filter: var(--gl-backdrop-2);
  backdrop-filter: var(--gl-backdrop-2);
  color: var(--gl-ink);
  box-shadow:
    inset 0 var(--gl-border-width) 0 0 var(--gl-highlight),
    var(--gl-shadow-2);
}

/* The ghost carries no surface at all until it is hovered, which is how the
   reader learns it was a control. It takes the boundary edge rather than the
   strong one, because at rest there is no boundary to identify. */
.gl-btn-ghost {
  background: transparent;
  color: var(--gl-ink);
  border-color: transparent;
  box-shadow: none;
}

.gl-btn-ghost:hover:not(:disabled) {
  background: var(--gl-surface-1);
  -webkit-backdrop-filter: var(--gl-backdrop-1);
  backdrop-filter: var(--gl-backdrop-1);
  border-color: var(--gl-edge-boundary);
  box-shadow: var(--gl-shadow-1);
}

.gl-btn-sm {
  min-height: 44px; /* the visual box shrinks, the touch target does not */
  padding: var(--gl-space-2) var(--gl-space-4);
  font-size: var(--gl-text-sm);
}

.gl-btn-lg {
  padding: var(--gl-space-4) var(--gl-space-8);
  font-size: var(--gl-text-lead);
  border-radius: var(--gl-radius-lg);
}

/* ==========================================================================
   CARD
   Variants: (base), gl-card-featured, gl-card-recessed
   Parts:    gl-card-header, gl-card-title, gl-card-body, gl-card-footer
   ========================================================================== */
.gl-card {
  display: flex;
  flex-direction: column;

  background: var(--gl-surface-2);
  -webkit-backdrop-filter: var(--gl-backdrop-2);
  backdrop-filter: var(--gl-backdrop-2);
  color: var(--gl-ink);

  border: var(--gl-border-width) solid var(--gl-edge-boundary);
  border-radius: var(--gl-radius-lg);
  box-shadow:
    inset 0 var(--gl-border-width) 0 0 var(--gl-highlight),
    var(--gl-shadow-2);

  /* One step wider than an opaque card would take. A frosted surface has no
     hard edge holding text off the boundary, so a tight glass panel reads as
     a smudge rather than as a pane with something written on it. */
  padding: var(--gl-space-8);
}

/* The featured card is where the theme spends its one glow. The glow is
   outside the shadow stack rather than replacing it, because a lit pane still
   casts. One of these per screen. */
.gl-card-featured {
  border-color: var(--gl-edge-strong);
  box-shadow:
    inset 0 var(--gl-border-width) 0 0 var(--gl-highlight),
    var(--gl-shadow-3),
    var(--gl-shadow-glow);
}

/* A well rather than a pane. Recessed surfaces drop to tier 1 and lose the
   top highlight, since a surface below the plane catches no light on its
   upper edge. The inset shadow is what sells the direction. */
.gl-card-recessed {
  background: var(--gl-surface-1);
  -webkit-backdrop-filter: var(--gl-backdrop-1);
  backdrop-filter: var(--gl-backdrop-1);
  border-color: var(--gl-edge);
  box-shadow: var(--gl-shadow-inset);
}

.gl-card-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--gl-space-4);
  margin-bottom: var(--gl-space-4);
}

.gl-card-title {
  margin: 0;
  font-size: var(--gl-text-h3);
  font-weight: var(--gl-weight-display);
  line-height: var(--gl-leading-snug);
  letter-spacing: var(--gl-tracking-tight);
}

.gl-card-body {
  margin: 0;
  color: var(--gl-ink-muted);
  font-size: var(--gl-text-body);
  line-height: var(--gl-leading-body);
}

.gl-card-footer {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--gl-space-3);
  margin-top: var(--gl-space-6);
}

/* A card that is itself a link. */
.gl-card-link {
  text-decoration: none;
  transition:
    transform var(--gl-duration) var(--gl-ease),
    box-shadow var(--gl-duration) var(--gl-ease),
    border-color var(--gl-duration) var(--gl-ease);
}

.gl-card-link:hover {
  transform: translateY(calc(var(--gl-lift) * -1));
  border-color: var(--gl-edge-strong);
  box-shadow:
    inset 0 var(--gl-border-width) 0 0 var(--gl-highlight),
    var(--gl-shadow-3);
}

.gl-card-link:focus-visible {
  outline: var(--gl-ring-width) solid var(--gl-ring);
  outline-offset: var(--gl-ring-offset);
}

/* ==========================================================================
   INPUT
   Parts:  gl-field, gl-label, gl-input, gl-hint, gl-error
   States: default, :focus, aria-invalid="true", :disabled
   ========================================================================== */
.gl-field {
  display: flex;
  flex-direction: column;
  gap: var(--gl-space-2);
}

.gl-label {
  font-family: var(--gl-font-sans);
  font-size: var(--gl-text-sm);
  font-weight: var(--gl-weight-bold);
  color: var(--gl-ink);
}

/* A field is a well, so it sits at tier 1 and takes an inset shadow. It also
   takes the strong edge rather than the boundary edge: a control a pointer
   can act on has to be identifiable at 3:1 against whatever is behind it, and
   at tier 1 the fill contributes 1.12:1 of that. The edge is carrying it. */
.gl-input {
  /* 16px minimum, which is what stops iOS Safari zooming on focus. */
  width: 100%;
  min-height: 44px;
  padding: var(--gl-space-3) var(--gl-space-4);

  font-family: var(--gl-font-sans);
  font-size: var(--gl-text-body);
  color: var(--gl-ink);

  background: var(--gl-surface-1);
  -webkit-backdrop-filter: var(--gl-backdrop-1);
  backdrop-filter: var(--gl-backdrop-1);

  border: var(--gl-border-width) solid var(--gl-edge-strong);
  border-radius: var(--gl-radius);
  box-shadow: var(--gl-shadow-inset);

  transition:
    background-color var(--gl-duration) var(--gl-ease),
    border-color var(--gl-duration) var(--gl-ease);
}

.gl-input::placeholder {
  color: var(--gl-ink-muted);
  opacity: 0.75;
}

/* Focus lifts the field from a well to a working surface. The fill goes up a
   tier and the inset shadow goes, so the field the reader is typing in is the
   one sitting level with the page. */
.gl-input:focus {
  outline: var(--gl-ring-width) solid var(--gl-ring);
  outline-offset: var(--gl-ring-offset);
  background: var(--gl-surface-2);
  -webkit-backdrop-filter: var(--gl-backdrop-2);
  backdrop-filter: var(--gl-backdrop-2);
  box-shadow: inset 0 var(--gl-border-width) 0 0 var(--gl-highlight);
}

.gl-input[aria-invalid="true"] {
  border-color: var(--gl-destructive);
}

.gl-input:disabled {
  color: var(--gl-ink-muted);
  border-color: var(--gl-edge);
  cursor: not-allowed;
  box-shadow: none;
}

.gl-hint {
  font-size: var(--gl-text-sm);
  color: var(--gl-ink-muted);
}

.gl-error {
  font-size: var(--gl-text-sm);
  font-weight: var(--gl-weight-bold);
  color: var(--gl-destructive);
}

.gl-input:where(textarea) {
  min-height: 120px;
  resize: vertical;
}

/* ==========================================================================
   BADGE
   Variants: gl-badge-neutral, gl-badge-success, gl-badge-info,
             gl-badge-warning, gl-badge-danger

   The one place in the theme where the alpha floor applies. A 12px uppercase
   label cannot absorb the ground's variation the way a paragraph can, so the
   fill is opaque enough that the fill, and not the backdrop, decides what the
   label is sitting on. Measured on the lit end of the ground, the same fills
   at a decorative 0.20 alpha put the danger label at 4.08:1. At the floor it
   measures 6.37:1. The floor is the whole difference.
   ========================================================================== */
.gl-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--gl-space-1);
  padding: var(--gl-space-1) var(--gl-space-2);

  font-family: var(--gl-font-sans);
  font-size: var(--gl-text-xs);
  font-weight: var(--gl-weight-bold);
  line-height: var(--gl-leading-snug);
  letter-spacing: var(--gl-tracking-wide);
  text-transform: uppercase;
  white-space: nowrap;

  border: var(--gl-border-width) solid var(--gl-edge);
  border-radius: var(--gl-radius-tight);

  /* No backdrop-filter. At the floor there is almost nothing to see through,
     so the blur would cost a compositing layer per badge and buy nothing. */
}

.gl-badge-neutral {
  background: color-mix(in srgb, var(--gl-fill-neutral) 62%, transparent);
  color: var(--gl-ink-neutral);
}

.gl-badge-success {
  background: color-mix(in srgb, var(--gl-fill-success) 62%, transparent);
  color: var(--gl-ink-success);
}

.gl-badge-info {
  background: color-mix(in srgb, var(--gl-fill-info) 62%, transparent);
  color: var(--gl-ink-info);
}

.gl-badge-warning {
  background: color-mix(in srgb, var(--gl-fill-warning) 62%, transparent);
  color: var(--gl-ink-warning);
}

.gl-badge-danger {
  background: color-mix(in srgb, var(--gl-fill-danger) 62%, transparent);
  color: var(--gl-ink-danger);
}

/* The 62% above is --gl-alpha-floor, written out because color-mix cannot
   take a percentage from a custom property. It is the one place in this file
   where a number appears rather than a token, and it is a number the verifier
   reads back out of tokens.css and checks against these fills.

   Where color-mix is unsupported (before Chrome 111, Safari 16.2, Firefox
   113) the background declaration is dropped, the badge falls back to no fill
   at all, and the label sits on the panel. Those labels still clear AA on
   every surface in this theme, so the degradation is a loss of color coding
   rather than a loss of legibility. */

/* ==========================================================================
   NAV
   A header bar with a brand block, inline links on desktop, and a disclosure
   panel on mobile. The disclosure is a <details> element, so the pattern
   needs no JavaScript and keeps working with the page's scripts disabled.

   The bar is sticky, which is the one place this register earns its keep at
   no cost: content scrolling under a frosted bar is the effect glass exists
   for, and it is the only animation in the theme that the reader drives.
   ========================================================================== */
.gl-nav {
  position: sticky;
  top: 0;
  z-index: 30;

  /* The scrim, and the one component in the theme that has to have it. What
     passes under a sticky bar is the document, not the ground, so the bound
     every other ratio in this theme rests on does not cover it. On a tier-2
     fill alone, a nav link with body text scrolling under it measures 1.24:1.
     The dark fill below is what puts a floor under an unbounded backdrop; the
     tier-2 white on top of it is what keeps the bar reading as a surface.
     See rule 3 in depth.css. */
  background-color: var(--gl-scrim);
  background-image: linear-gradient(var(--gl-surface-2), var(--gl-surface-2));
  -webkit-backdrop-filter: var(--gl-backdrop-2);
  backdrop-filter: var(--gl-backdrop-2);
  border-bottom: var(--gl-border-width) solid var(--gl-edge-boundary);
  box-shadow: inset 0 var(--gl-border-width) 0 0 var(--gl-highlight);
}

.gl-nav-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gl-space-4);
  max-width: var(--gl-container);
  margin: 0 auto;
  padding: var(--gl-space-3) var(--gl-space-4);
}

.gl-nav-brand {
  display: inline-flex;
  align-items: center;
  gap: var(--gl-space-3);
  font-family: var(--gl-font-sans);
  font-size: var(--gl-text-h3);
  font-weight: var(--gl-weight-display);
  letter-spacing: var(--gl-tracking-tight);
  color: var(--gl-ink);
  text-decoration: none;
}

/* The brand mark is a lit pane, which is the smallest statement of the whole
   register: a translucent square with a catch-light and a glow behind it. */
.gl-nav-mark {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  color: var(--gl-accent);
  background: var(--gl-surface-3);
  border: var(--gl-border-width) solid var(--gl-edge-strong);
  border-radius: var(--gl-radius-tight);
  box-shadow:
    inset 0 var(--gl-border-width) 0 0 var(--gl-highlight),
    var(--gl-shadow-glow);
  font-size: var(--gl-text-body);
  font-weight: var(--gl-weight-display);
}

.gl-nav-links {
  display: none;
  align-items: center;
  gap: var(--gl-space-1);
  list-style: none;
  margin: 0;
  padding: 0;
}

.gl-nav-link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: var(--gl-space-2) var(--gl-space-3);

  font-family: var(--gl-font-sans);
  font-size: var(--gl-text-sm);
  font-weight: var(--gl-weight-bold);
  color: var(--gl-ink-muted);
  text-decoration: none;

  border: var(--gl-border-width) solid transparent;
  border-radius: var(--gl-radius);
  transition:
    color var(--gl-duration) var(--gl-ease),
    background-color var(--gl-duration) var(--gl-ease);
}

.gl-nav-link:hover {
  color: var(--gl-ink);
  background: var(--gl-surface-1);
}

/* The current page is marked with ink and an edge rather than with a fill,
   because the bar is already a surface and a second fill inside it reads as a
   button. */
.gl-nav-link[aria-current="page"] {
  color: var(--gl-ink);
  background: var(--gl-surface-1);
  border-color: var(--gl-edge-boundary);
}

.gl-nav-link:focus-visible {
  outline: var(--gl-ring-width) solid var(--gl-ring);
  outline-offset: var(--gl-ring-offset);
}

.gl-nav-cta {
  display: none;
}

/* Mobile disclosure */
.gl-nav-disclosure {
  position: relative;
}

.gl-nav-disclosure > summary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--gl-space-2);
  min-width: 44px;
  min-height: 44px;
  padding: var(--gl-space-2) var(--gl-space-3);

  font-family: var(--gl-font-sans);
  font-size: var(--gl-text-sm);
  font-weight: var(--gl-weight-bold);
  color: var(--gl-ink);
  background: var(--gl-surface-1);

  border: var(--gl-border-width) solid var(--gl-edge-strong);
  border-radius: var(--gl-radius);
  cursor: pointer;
  list-style: none;
}

.gl-nav-disclosure > summary::-webkit-details-marker {
  display: none;
}

.gl-nav-disclosure > summary:focus-visible {
  outline: var(--gl-ring-width) solid var(--gl-ring);
  outline-offset: var(--gl-ring-offset);
}

/* The panel floats over the bar, so it is the one component that sits at tier
   3. It carries no backdrop-filter of its own: it opens inside .gl-nav, which
   is already blurring, and a second filter would blur that result rather than
   the page. See the stacking rule in depth.css. */
.gl-nav-panel {
  position: absolute;
  right: 0;
  z-index: 40;
  min-width: 220px;
  margin-top: var(--gl-space-2);
  padding: var(--gl-space-2);

  list-style: none;
  /* The panel hangs below the bar, so most of it is over page content rather
     than over the nav. Rule 3 applies to it for the same reason it applies to
     the bar, and more obviously: a menu you can read the page through is a
     menu nobody can read. */
  background-color: var(--gl-scrim);
  background-image: linear-gradient(var(--gl-surface-3), var(--gl-surface-3));
  border: var(--gl-border-width) solid var(--gl-edge-boundary);
  border-radius: var(--gl-radius-lg);
  box-shadow:
    inset 0 var(--gl-border-width) 0 0 var(--gl-highlight),
    var(--gl-shadow-3);
}

.gl-nav-panel .gl-nav-link {
  display: flex;
  width: 100%;
}

/* At 768px the links come inline and the disclosure retires. */
@media (min-width: 768px) {
  .gl-nav-inner {
    padding: var(--gl-space-3) var(--gl-space-6);
  }

  .gl-nav-links,
  .gl-nav-cta {
    display: flex;
  }

  .gl-nav-disclosure {
    display: none;
  }
}
