/* Shared widget frame. Every puzzle reads these; a theme re-skins them all.

   Interaction contract (enforced by interaction-audit.cjs):
   - every :hover lives in @media (hover: hover) — on touch it otherwise sticks
     to the last-tapped element and reads as a stuck selection;
   - one state, one channel: selected = ring, linked = left edge, excluded =
     dimmed. Never signal a state with border-color alone — two states collide
     on it and a deselected element looks selected.
   See PUZZLE_WIDGET_SPEC.md §8-9a.

   Lives in the `base` layer (DESIGN_SYSTEM.md §7.1): element defaults and the
   shared frame — `.ui-stage`, `.ui-tap`, `.ui-input`, `.ui-is-*` — but no reset
   (that is workbench `reset`). */

@layer base {

/* Every interactive element gets touch behaviour without each widget
   remembering it. */
.ui-widget button,
.ui-widget input,
.ui-widget select,
.ui-widget [role="button"],
.ui-widget [role="radio"],
.ui-widget [role="checkbox"] {
  touch-action: manipulation;
}

.ui-tap {
  min-width: 44px;
  min-height: 44px;
  touch-action: manipulation;
}

/* 16px: below this iOS Safari zooms the page on focus. */
.ui-input {
  font-size: var(--ui-font-size-16);
  touch-action: manipulation;
}

.ui-is-selected {
  box-shadow: 0 0 0 2px var(--ui-color-heading);
}

.ui-is-linked {
  border-left: 4px solid var(--ui-color-success);
}

.ui-is-excluded {
  opacity: 0.45;
  text-decoration: line-through;
  cursor: default;
}

/* Non-colour channel, so "solved" is not signalled by colour alone (§8). */
.ui-is-solved {
  color: var(--ui-color-success);
  border-color: var(--ui-color-success-border);
  box-shadow: inset 0 0 0 1px var(--ui-color-success-border);
}

/* The widget's local status region (DESIGN_SYSTEM.md §8.1, §11.11;
   PUZZLE_WIDGET_SPEC.md §8). Every widget carries ONE of these: it is present
   in the DOM, EMPTY at mount, and its text content is REPLACED by a local
   message (progress, validation). It is NOT the host's verdict announcer — the
   host owns solved/incorrect/transport, the widget owns only its local state —
   so a widget must never write a verdict here.

   Visually hidden (§8.1 explicitly allows this): the region exists for screen
   readers, not the layout. Absolute positioning takes it out of flow, so adding
   it cannot move any other box. Deliberately no `display:none` — an element
   that is hidden until its text changes is exactly the host-banner
   anti-pattern §8.1 warns against; this region is always in the accessibility
   tree. A widget that wants a VISIBLE status line (evidence_board, sudoku_grid)
   styles its own element instead of using this class. */
.ui-status {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* The stage is the widget's query container (§7.3). A container query needs a
   DEFINITE inline size, so the stage takes its width from the host column
   (`#stage`, workbench.css) rather than shrink-to-fitting its content: a
   `container-type: inline-size` element cannot size itself from its contents.
   `max-width` keeps the presentation column from stretching on a wide screen;
   widgets then size themselves from `cqw` instead of the viewport or the host's
   alignment (§10.7). */
.ui-stage {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--ui-space-14);
  width: 100%;
  max-width: 420px;
  margin-inline: auto;
  padding: var(--ui-space-16);
  border-radius: var(--ui-radius-lg);
  background: var(--ui-color-surface);
  border: 1px solid var(--ui-color-border);
  container-type: inline-size;
}

.ui-title {
  font-family: var(--ui-font-family-heading);
  font-size: var(--ui-font-size-17);
  font-weight: 700;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--ui-color-heading);
  text-align: center;
}

.ui-instructions {
  font-family: var(--ui-font-family-body);
  font-size: var(--ui-font-size-13);
  color: var(--ui-color-text-muted);
  text-align: center;
  max-width: 320px;
}

.ui-widget {
  font-family: var(--ui-font-family-body);
}

/* §11.3 visible focus, using the §3.6 focus token. The widget roots
   (safe_dial, sudoku_grid) and their controls suppress the UA default outline,
   so without this there is no visible focus indication at all. `:focus-visible`
   (not `:focus`) keeps the ring for keyboard users while a pointer click does
   not paint it — the standard pattern, and it costs no layout. */
.ui-widget:focus-visible,
.ui-widget :focus-visible {
  outline: 3px solid var(--ui-color-focus);
  outline-offset: 2px;
}

@keyframes ui-shake {
  0%, 100% { transform: translateX(0); }
  20% { transform: translateX(-6px); }
  40% { transform: translateX(6px); }
  60% { transform: translateX(-4px); }
  80% { transform: translateX(4px); }
}
.ui-widget.shake { animation: ui-shake var(--ui-motion-duration-shake); }

/* §11.6 / check L: honour the OS reduced-motion preference. The shake is
   interaction feedback, so it is disabled outright rather than shortened. */
@media (prefers-reduced-motion: reduce) {
  .ui-widget.shake { animation: none; }
}
}
