/* ══════════════════════════════════════════════════════════
   THE WRITE SURFACE — the portal's one form container
   css/system/components/write-surface.css

   Movement A of the snag arcs: two surfaces, one discipline.
   The peek reads (peek.css); this writes. There is no third
   device, and no popup deviates.

   ONE COMPONENT, TWO POSTURES, switched by viewport:
     desktop      — floating panel, capped at 85dvh
     below 640px  — full-screen sheet at 100dvh

   THREE ZONES, always: a pinned header, an internally
   scrolling body, a pinned action bar. The bar is on screen
   at every scroll position at every viewport, which is the
   whole reason the body scrolls rather than the panel.

   dvh, NEVER vh. Mobile browser chrome counts against vh, so
   a vh-sized sheet puts the save bar under the address bar —
   which is the exact failure this component exists to end.

   Behaviour (dismiss discipline, snapshot-diff dirty state)
   lives in WriteSurface (js/system/components/write-surface.js).
   Any host emitting these classes must link this file —
   scripts/check-css-contract.py enforces it.
   ══════════════════════════════════════════════════════════ */

/* ── the scrim ─────────────────────────────────────────────
   Sits above the peek (z 150) so a card can never overlap an
   open form. */
.ws-overlay {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--s-5);
  background: rgba(0, 0, 0, 0.55);
  animation: ws-fade var(--dur-fast) var(--ease-std) both;
}

/* ── the panel (desktop posture) ───────────────────────────
   A grid of three rows: head / body / bar. The body is the
   only row allowed to grow, so the other two stay pinned
   without position tricks. */
.ws {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  width: 100%;
  max-width: 560px;
  max-height: 85dvh;
  background: var(--paper);
  border: 1px solid var(--rule-2);
  border-radius: var(--r-2);
  box-shadow: var(--shadow-3);
  overflow: hidden;
  animation: ws-pop var(--dur-med) var(--ease-out) both;
}
.ws--wide { max-width: 760px; }

/* ── the two-pane variant (combined subhire editor authority, b2) ──
   A WIDTH AND NOTHING ELSE. The dismiss discipline lives on the overlay
   handlers and tests `panelRef.contains(e.target)`, which knows nothing
   about how wide the panel is — so this variant inherits mousedown-origin
   dismissal by construction rather than by remembering to. That is the
   whole reason the authority put the frame in the shared component
   instead of building a bespoke dialog: a second popup system is a second
   place for the drag-to-select bug to come back.

   1160px carries the 380px spine plus a legs pane wide enough for a
   three-across date/time/mode row without wrapping. Below 900px the grid
   inside collapses to one column (see .sbe-grid) — that is a breakpoint,
   not a second design, so nothing here changes at that width except the
   panel filling the viewport as every posture already does. */
.ws--xwide { max-width: 1160px; }

/* ── zone 1: the pinned header ─────────────────────────── */
.ws__head {
  display: flex;
  align-items: baseline;
  gap: var(--s-3);
  padding: var(--s-5) var(--s-5) var(--s-4);
  border-bottom: 1px solid var(--rule);
}
.ws__title {
  flex: 1 1 auto;
  margin: 0;
  font-family: var(--font-ex);
  font-weight: var(--w-bold);
  font-size: var(--t-h5);
  line-height: var(--lh-snug);
  letter-spacing: var(--track-sub, -0.03em);
  text-transform: lowercase;
  color: var(--ink);
}
.ws__sub {
  font-family: var(--font-cd);
  font-size: var(--t-micro);
  letter-spacing: 0.04em;
  color: var(--ink-3);
}
.ws__close {
  flex: 0 0 auto;
  appearance: none;
  border: 0;
  background: none;
  padding: 0 0 0 var(--s-3);
  font-size: 20px;
  line-height: 1;
  color: var(--ink-4);
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease-std);
}
.ws__close:hover { color: var(--ink); }

/* ── zone 2: the scrolling body ──────────────────────────
   The ONLY scroll container. overscroll-behavior contain
   stops a flick at the end of the list scrolling the page
   behind the sheet. */
.ws__body {
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--s-5);
  font-family: var(--font-std);
  font-size: var(--t-small);
  color: var(--ink);
}

/* ── zone 3: the pinned action bar ───────────────────────── */
.ws__bar {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--s-2);
  padding: var(--s-3) var(--s-5);
  border-top: 1px solid var(--rule);
  background: var(--paper-2);
}
.ws__barnote {
  flex: 1 1 auto;
  font-family: var(--font-cd);
  font-size: var(--t-micro);
  letter-spacing: 0.04em;
  color: var(--ink-3);
}

/* the pulse: a dirty form refuses a click-outside and says so
   HERE, in the bar, rather than in a second dialog. One shot,
   then it settles. */
.ws__bar.is-pulsing {
  animation: ws-pulse 620ms var(--ease-std) 1;
}
@keyframes ws-pulse {
  0%   { background: var(--paper-2); }
  18%  { background: var(--accent-soft); }
  100% { background: var(--paper-2); }
}

/* the inline discard question — the bar SWAPS to this. Never a
   dialog stacked on a dialog. */
.ws__bar.is-asking { background: var(--accent-soft); }
.ws__ask {
  flex: 1 1 auto;
  font-family: var(--font-cd);
  font-size: var(--t-small);
  letter-spacing: 0.02em;
  color: var(--ink);
}

/* ── the sheet posture ─────────────────────────────────────
   Below 640px the same component is a full-screen sheet. Only
   the geometry changes; every behaviour above is identical. */
@media (max-width: 639px) {
  .ws-overlay {
    padding: 0;
    align-items: stretch;
    justify-content: stretch;
  }
  .ws {
    max-width: none;
    width: 100%;
    height: 100dvh;
    max-height: 100dvh;
    border: 0;
    border-radius: 0;
    animation: ws-rise var(--dur-med) var(--ease-out) both;
  }
  /* thumb-reachable and unmissable at the bottom of a sheet */
  .ws__bar {
    padding: var(--s-4) var(--s-4);
    padding-bottom: max(var(--s-4), env(safe-area-inset-bottom));
  }
  .ws__head { padding: var(--s-4); }
  .ws__body { padding: var(--s-4); }
}

@keyframes ws-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes ws-pop  { from { opacity: 0; transform: scale(0.97); } to { opacity: 1; transform: scale(1); } }
@keyframes ws-rise { from { transform: translateY(2%); } to { transform: translateY(0); } }

@media (prefers-reduced-motion: reduce) {
  .ws-overlay, .ws { animation: none; }
  .ws__bar.is-pulsing { animation: none; outline: 2px solid var(--accent); outline-offset: -2px; }
}

/* ══════════════════════════════════════════════════════════
   THE NOTES DEVICE — one dot, everywhere
   The dot is PRESENCE, not category: one dot whether one field
   or both carry content, because two dots would be noise. The
   reveal does the labelling.
   ══════════════════════════════════════════════════════════ */

.ws-note-dot {
  display: inline-block;
  width: 6px;
  height: 6px;
  flex: 0 0 auto;
  border-radius: var(--r-pill);
  background: var(--zeal-orange);
  vertical-align: middle;
}
/* on a bar or tile the dot rides the corner rather than the text flow */
.ws-note-dot--pin {
  position: absolute;
  top: 4px;
  right: 4px;
}

/* the desktop hover reveal, and the same content as a peek
   section on touch — one device, two placements */

/* The React reveal's wrapper. It had NO rule until the projects
   dashboard became the first page to load this module and the CSS
   contract checker said so — the HTML-string form (notesRevealHTML,
   used by the peek card) emits the .ws-note children straight into a
   host container and never needed a wrapper, so the gap sat unnoticed
   in the React path nothing had mounted yet. The first .ws-note owns
   its own top border, so the wrapper only has to stop collapsing it. */
.ws-notes { display: block; }
.ws-notes > .ws-note:first-child { margin-top: 0; }

.ws-note {
  margin-top: var(--s-2);
  padding-top: var(--s-2);
  border-top: 1px solid var(--rule);
}
.ws-note__label {
  font-family: var(--font-cd);
  font-size: var(--t-micro);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin-bottom: 2px;
}
.ws-note__body {
  font-family: var(--font-std);
  font-size: var(--t-small);
  line-height: var(--lh-relaxed, 1.4);
  color: var(--ink-2);
  white-space: pre-wrap;
}
/* the labelled pair. Only rendered where the project/warehouse
   pair exists; a single-notes surface gets no label at all,
   because a lone "notes" label says nothing. */
.ws-note + .ws-note { margin-top: var(--s-3); }

/* the edit affordance the peek carries on touch — the ONLY
   route from a read to a write */
.ws-note__edit,
.ws-peek-edit {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin-top: var(--s-2);
  padding: 4px 10px;
  border: 1px solid var(--rule-2);
  border-radius: var(--r-soft);
  background: var(--paper);
  font-family: var(--font-cd);
  font-size: var(--t-micro);
  letter-spacing: 0.04em;
  color: var(--ink);
  cursor: pointer;
}
.ws-note__edit:hover,
.ws-peek-edit:hover { border-color: var(--ink-4); }

/* ── the destructive corner (footExtra) ───────────────────────────
   A delete that belongs to the thing being edited, pinned to the far
   left of the bar so it can never be mistaken for the save at the other
   end. Lives here rather than with any one caller's stylesheet because
   write-surface.js is what emits the class — a class and the script that
   writes it belong to the same module, which is exactly what
   scripts/check-css-contract.py exists to keep true. */
.ws__barextra { margin-right: auto; }
