/* os/wm.css — windows.
 *
 * The desktop metaphor was borrowed without the chrome. This file supplies the
 * chrome that makes a rectangle read as an operating-system window rather than a
 * card: a titlebar with a drag surface, three traffic lights that mean what
 * traffic lights mean, eight resize edges you can see and grab, a focus ring that
 * is the loudest thing on the screen, and a maximised state that removes every
 * decoration because there is no longer a rectangle to decorate.
 *
 * The direction stays ink-on-paper. Nothing here is grey corporate chrome: the
 * outline is the same ink as every icon, the paper is the same paper, and the
 * shadow is the hard offset shadow the rest of the site already draws with. What
 * is new is that the shadow and the ring now CARRY STATE — deep and ringed when
 * the window is focused, flat and grey when it is not, so focus is legible from
 * across the desk without reading a single word.
 *
 * ── contract (site-v4/CONTRACTS.md) ─────────────────────────────────────────
 *   root            <section data-window data-window-id="<id>">
 *                   The window manager writes left/top/width/height inline in px
 *                   on the root. This file does NOT set them — an inline
 *                   declaration beats a stylesheet declaration, so a
 *                   `left: var(--x)` here would silently do nothing. CSS owns
 *                   the transitions, the states and the chrome.
 *   states          .is-focused  .is-minimized  .is-maximized
 *                   plus .is-dragging / .is-resizing, which are transient pointer
 *                   states set for the duration of a gesture.
 *   bar             .win__bar — the drag surface, focusable (tabindex=0) so a
 *                   window can be moved without a pointer
 *   lights          .win__lights > .win__light[data-act=minimize|maximize|close]
 *   text            .win__icon .win__title .win__sub
 *   content         .win__body — zero padding, so app padding never fights
 *   footer          .win__actions
 *   grips           .win__grip[data-edge=n|s|e|w|ne|nw|se|sw]
 *
 *   The previous shell's window carries .app and the .app__* children. They are
 *   aliased through :is() at every level so one desktop renders correctly
 *   whichever markup built it — a window that looks different depending on which
 *   shell produced it is a bug with a delayed fuse.
 *
 * ── layering ────────────────────────────────────────────────────────────────
 *   The z-scale is declared in desktop.css on the OS root and read here as
 *   --z-win / --z-win-focus. Nothing in this file sets a z-index outside a
 *   window.
 * ────────────────────────────────────────────────────────────────────────── */


/* ══ tokens ════════════════════════════════════════════════════════════════
 * On :root rather than on .win, so a maximised window, a titlebar and a traffic
 * light all quote the same numbers and a theme can move them once. */

:root {
  /* the three lights, in the site's own palette rather than the system's */
  --win-light-close: #E2564A;
  --win-light-min: #E0A43C;
  --win-light-max: #6BA368;

  --win-radius: 14px;
  --win-bar-h: 2.9rem;
  --win-edge: 10px;         /* grab thickness of a resize edge */
  --win-grip: 20px;         /* grab thickness of a resize corner */

  /* the focus ring, per the contract: 3px solid, and driven by the theme's
     accent so opening a different business re-tints the whole OS including the
     ring. --violet is only the pre-boot default. */
  --win-focus: var(--accent, var(--violet, #6D5BD0));

  /* two shadows, both used by every state below: a hard offset for the printed
     look, a soft one for depth. Resting, focused and dragging each pick a mix. */
  --win-shadow-rest:
    4px 5px 0 -2px rgba(23, 21, 31, .16),
    0 10px 22px -14px rgba(23, 21, 31, .45);
  --win-shadow-focus:
    10px 12px 0 -4px rgba(23, 21, 31, .3),
    0 34px 64px -26px rgba(23, 21, 31, .55);
  --win-shadow-drag:
    16px 20px 0 -6px rgba(23, 21, 31, .34),
    0 52px 90px -30px rgba(23, 21, 31, .6);
}


/* ══ the window ════════════════════════════════════════════════════════════ */

:is([data-window], .app) {
  position: absolute;
  z-index: var(--z-win, 10);

  /* the manager's inline geometry is authoritative; these only stop a window
     from being dragged outside the desktop and stranded there */
  max-width: 100%;
  max-height: 100%;

  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;

  border: 2px solid rgba(23, 21, 31, .38);
  border-radius: var(--win-radius);
  background: var(--paper, #F6F1E7);
  color: var(--ink, #17151F);
  box-shadow: var(--win-shadow-rest);

  /* geometry and paint are the window's own business — nothing outside it can
     invalidate either, and the manager drags this node 60 times a second */
  contain: layout paint style;

  /* the whole transition contract for a window, in one declaration. Opening and
     closing override it, because they animate scale and opacity, which a
     transition on inset cannot produce. */
  transition:
    left .26s cubic-bezier(.2, .9, .25, 1),
    top .26s cubic-bezier(.2, .9, .25, 1),
    width .26s cubic-bezier(.2, .9, .25, 1),
    height .26s cubic-bezier(.2, .9, .25, 1),
    border-color .2s ease,
    border-radius .22s ease,
    box-shadow .22s ease,
    opacity .2s ease;
}

/* Focused. The ring is drawn OUTSIDE the border so it never eats layout, and it
   is the strongest thing on the desktop — which is the point: focus must be
   obvious before you read anything. */
:is([data-window], .app).is-focused {
  z-index: var(--z-win-focus, 11);
  border-color: var(--ink, #17151F);
  box-shadow:
    0 0 0 3px var(--win-focus),
    var(--win-shadow-focus);
}

/* ── entering and leaving ───────────────────────────────────────────────────
 * scale + opacity as keyframes, because a transition cannot hold the window at
 * the start of an inset animation without also animating its position. */

:is([data-window], .app).is-opening {
  animation: win-open .3s cubic-bezier(.2, .9, .25, 1) both;
  transition: none;
}
@keyframes win-open {
  from { opacity: 0; scale: .9; translate: 0 12px; }
  to   { opacity: 1; scale: 1;  translate: 0; }
}

:is([data-window], .app).is-closing {
  animation: win-close .2s cubic-bezier(.6, 0, .8, .3) both;
  transition: none;
}
@keyframes win-close {
  to { opacity: 0; scale: .93; }
}

/* ── minimised ─────────────────────────────────────────────────────────────
 * A minimised window is not hidden, it is docked: it shrinks toward the taskbar
 * edge and gives its place in the stacking order back. The taskbar entry is the
 * other half of this state. */

:is([data-window], .app).is-minimized {
  opacity: 0;
  scale: .7;
  translate: 0 60%;
  pointer-events: none;
  transition:
    opacity .18s ease,
    scale .22s cubic-bezier(.6, 0, .8, .3),
    translate .22s cubic-bezier(.6, 0, .8, .3);
}


/* ══ titlebar ══════════════════════════════════════════════════════════════
 * The one place in a window a drag may start. It opts out of text selection,
 * because a drag that selects the title is the most common way a hand-rolled
 * window feels broken. */

:is(.win__bar, .app__bar) {
  flex: none;
  min-height: var(--win-bar-h);
  display: flex;
  align-items: center;
  gap: .7rem;
  padding: .3rem .5rem .3rem .55rem;

  border-bottom: 2px solid var(--ink, #17151F);
  border-radius: calc(var(--win-radius) - 2px) calc(var(--win-radius) - 2px) 0 0;
  background: var(--paper-2, #EFE7D9);

  cursor: grab;
  user-select: none;
  -webkit-user-select: none;
  touch-action: none;
}

/* Unfocused windows recede three ways at once: the bar loses its ink, the title
   loses its weight, and the lights go grey. With two windows open, a user must be
   able to say which one the keystrokes are going to. */
:is([data-window], .app):not(.is-focused) :is(.win__bar, .app__bar) {
  border-bottom-color: rgba(23, 21, 31, .28);
  background: #F2ECE0;
}
:is([data-window], .app):not(.is-focused) :is(.win__title, .app__title) {
  color: rgba(23, 21, 31, .58);
}

/* The titlebar is the keyboard drag handle. A window that can only be dragged
   with a mouse is not a window, it is a picture of one. */
:is(.win__bar, .app__bar):focus-visible {
  outline: 3px solid var(--win-focus);
  outline-offset: -3px;
}

/* Dragging: cursor held, window lifted, transition dropped so the window tracks
   the pointer with nothing between them. A 260ms easing curve on a live drag is
   lag, not animation. */
:is([data-window], .app).is-dragging {
  cursor: grabbing;
  box-shadow:
    0 0 0 3px var(--win-focus),
    var(--win-shadow-drag);
  transition: box-shadow .22s ease;
  user-select: none;
  -webkit-user-select: none;
}
:is([data-window], .app).is-dragging :is(.win__bar, .app__bar) { cursor: grabbing; }


/* ══ traffic lights ═════════════════════════════════════════════════════════
 * Three ink-ringed dots, in the order every desktop has used for thirty years:
 * close, minimize, maximize. The glyph is always visible rather than revealed on
 * hover — a hover reveal is a nicety, and a nicety carrying the only label on an
 * icon-only button is a nicety that fails in high contrast. */

.win__lights {
  flex: none;
  display: flex;
  align-items: center;
  gap: .38rem;
  /* decoration: never a hit target, and never the thing a drag starts on */
  pointer-events: none;
}

.win__light {
  /* the group turns pointer-events off for its own padding; each button takes
     it straight back, so the dots are clickable and the gap between them is not */
  pointer-events: auto;
  position: relative;
  display: grid;
  place-items: center;
  width: 15px;
  height: 15px;
  padding: 0;
  border: 2px solid var(--ink, #17151F);
  /* hand-drawn, not machined — the same cut as the icon selection loop */
  border-radius: 50% 48% 52% 50% / 50% 52% 48% 50%;
  background: var(--paper-2, #EFE7D9);
  color: var(--ink, #17151F);
  cursor: pointer;
  transition: background-color .16s ease, scale .16s ease;
}

/* Each glyph is one or two strokes drawn with a pseudo-element. ✕ is two, − is
   one bar, □ is an outline that gains a second square when the window is already
   maximised — the one piece of state a traffic light should carry. */
.win__light::before,
.win__light::after {
  content: "";
  position: absolute;
  background: var(--ink, #17151F);
  opacity: .8;
  pointer-events: none;
  transition: opacity .16s ease;
}
/* ✕ */
.win__light[data-act="close"]::before,
.win__light[data-act="close"]::after {
  background: none;
  border-top: 1.6px solid var(--ink, #17151F);
  inset: 50% 3px auto;
  height: 0;
  scale: 1 .8;
}
.win__light[data-act="close"]::before { rotate: 45deg; }
.win__light[data-act="close"]::after  { rotate: -45deg; }
/* − */
.win__light[data-act="minimize"]::before {
  inset: 50% 3px auto;
  height: 1.8px;
}
/* □ */
.win__light[data-act="maximize"]::before {
  inset: 3px;
  background: none;
  border: 1.6px solid var(--ink, #17151F);
  border-radius: 2px;
}
.is-maximized .win__light[data-act="maximize"]::before { scale: .62; }
.is-maximized .win__light[data-act="maximize"]::after {
  inset: 4px;
  background: none;
  border: 1.6px solid var(--ink, #17151F);
  border-radius: 2px;
}

/* Fill on hover: the light tells you what it does by becoming that colour. */
@media (hover: hover) {
  .win__light:hover { scale: 1.14; }
  .win__light[data-act="close"]:hover    { background: var(--win-light-close); }
  .win__light[data-act="minimize"]:hover { background: var(--win-light-min); }
  .win__light[data-act="maximize"]:hover { background: var(--win-light-max); }
}
.win__light:active { scale: .95; }

/* Unfocused: grey. The strongest focus cue after the ring itself, and it costs
   one filter. */
:is([data-window], .app):not(.is-focused) .win__light {
  border-color: rgba(23, 21, 31, .3);
  background: #E3DBCB;
  filter: grayscale(1);
}
:is([data-window], .app):not(.is-focused) .win__light::before,
:is([data-window], .app):not(.is-focused) .win__light::after { opacity: 0; }

/* Focus ring on an icon-only control. Drawn outside the dot so it survives both
   the coloured dot it surrounds and a grey one. */
.win__light:focus-visible {
  outline: 3px solid var(--ink, #17151F);
  outline-offset: 3px;
  filter: none;
  scale: 1.1;
}


/* ══ title text ═════════════════════════════════════════════════════════════ */

:is(.win__icon, .app__logo) {
  flex: none;
  display: block;
  width: 22px;
  height: 22px;
}
:is(.win__icon, .app__logo) svg { width: 100%; height: 100%; display: block; }

.win__titles,
.app__titles {
  flex: 1;
  min-width: 0;
  display: grid;
}

:is(.win__title, .app__title) {
  font-family: var(--serif, "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif);
  font-size: .95rem;
  font-weight: 700;
  letter-spacing: -.01em;
  line-height: 1.2;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
:is(.win__sub, .app__trade) {
  font-size: .68rem;
  line-height: 1.25;
  color: rgba(23, 21, 31, .6);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* In-titlebar controls (prev / next). They sit on the titlebar's rules, not the
   page's. */
:is(.win__actions, .app__wins) {
  flex: none;
  display: flex;
  align-items: center;
  gap: .35rem;
}
:is(.win__actions, .app__wins) :is(button, a) {
  padding: .3rem .55rem;
  border: 1.5px solid var(--ink, #17151F);
  border-radius: 8px;
  background: var(--paper, #F6F1E7);
  color: var(--ink, #17151F);
  font: inherit;
  font-size: .72rem;
  font-weight: 700;
  white-space: nowrap;
  cursor: pointer;
}
@media (hover: hover) {
  :is(.win__actions, .app__wins) :is(button, a):hover { background: var(--ink, #17151F); color: var(--paper, #F6F1E7); }
}
:is(.win__actions, .app__wins) :is(button, a):focus-visible {
  outline: 3px solid var(--win-focus);
  outline-offset: 2px;
}


/* ══ body ══════════════════════════════════════════════════════════════════
 * Zero padding, deliberately. The app owns its padding; a window that also pads
 * its content makes the content sit twice as far from the edge as it looks like
 * it does. min-height:0 is what lets a flex child scroll instead of growing the
 * window past its declared height. */

:is(.win__body, .app__view) {
  flex: 1;
  min-height: 0;
  position: relative;
  background: var(--paper, #F6F1E7);
  overflow: auto;
  overscroll-behavior: contain;
}


/* ══ resize grips ═══════════════════════════════════════════════════════════
 * Eight of them, LAST CHILD of the window root, and inset INSIDE it rather than
 * hung off the outside. The root is overflow:hidden — it has to be, or a world
 * inside it would spill over the rounded corners — and an overflow clip also
 * clips absolutely positioned descendants, so a grip hanging outside the border
 * would be unreachable at the window's edge. Inside costs nothing: the grip
 * overlays the border and the first few pixels of content, which is exactly what
 * a resize edge is.
 *
 * They are transparent except for the corner ticks, and a window that shows its
 * own hit-test grid is a CAD drawing. */

.win__grip {
  position: absolute;
  z-index: 3;
  touch-action: none;
}
.win__grip[data-edge="n"]  { inset: 0 0 auto 0; height: var(--win-edge); cursor: ns-resize; }
.win__grip[data-edge="s"]  { inset: auto 0 0 0; height: var(--win-edge); cursor: ns-resize; }
.win__grip[data-edge="w"]  { inset: 0 auto 0 0; width: var(--win-edge); cursor: ew-resize; }
.win__grip[data-edge="e"]  { inset: 0 0 0 auto; width: var(--win-edge); cursor: ew-resize; }
.win__grip[data-edge="nw"] { inset: 0 auto auto 0; width: var(--win-grip); height: var(--win-grip); cursor: nwse-resize; }
.win__grip[data-edge="ne"] { inset: 0 0 auto auto; width: var(--win-grip); height: var(--win-grip); cursor: nesw-resize; }
.win__grip[data-edge="sw"] { inset: auto auto 0 0; width: var(--win-grip); height: var(--win-grip); cursor: nesw-resize; }
.win__grip[data-edge="se"] { inset: auto 0 0 auto; width: var(--win-grip); height: var(--win-grip); cursor: nwse-resize; }

/* Two ticks in the corner that owns the grip — the printed equivalent of the
   diagonal hatch every window manager draws, and the reason a visitor who has
   never resized a window before will try it. */
.win__grip[data-edge="nw"]::after,
.win__grip[data-edge="se"]::after,
.win__grip[data-edge="ne"]::after,
.win__grip[data-edge="sw"]::after {
  content: "";
  position: absolute;
  width: 15px;
  height: 15px;
  opacity: 0;
  transition: opacity .18s ease;
  background:
    linear-gradient(135deg, transparent 0 46%, var(--ink, #17151F) 46% 54%, transparent 54% 100%),
    linear-gradient(135deg, transparent 0 74%, var(--ink, #17151F) 74% 82%, transparent 82% 100%);
}
.win__grip[data-edge="nw"]::after { inset: auto 4px 4px auto; }
.win__grip[data-edge="se"]::after { inset: auto auto auto 4px; transform: rotate(180deg); }
.win__grip[data-edge="ne"]::after { inset: 4px auto auto 4px; transform: scaleX(-1); }
.win__grip[data-edge="sw"]::after { inset: auto 4px auto 4px; transform: scaleY(-1); }

.is-focused .win__grip::after { opacity: .5; }
@media (hover: hover) {
  .win__grip:hover::after { opacity: 1; }
}

/* A grip can be focused, so every corner is reachable without a pointer. It
   carries no visual of its own — the ring is the whole affordance. */
.win__grip:focus-visible {
  outline: 3px solid var(--win-focus);
  outline-offset: -3px;
}

/* Resizing: the transition comes off (same reason as dragging) and the cursor is
   held, because the edge under the pointer changes as the window grows under it. */
:is([data-window], .app).is-resizing {
  transition: none;
  user-select: none;
  -webkit-user-select: none;
  will-change: width, height;
}


/* ══ maximised ═════════════════════════════════════════════════════════════
 * A maximised window is not a big window. It is the desktop with a titlebar on
 * it: no radius (a screen has square corners), no shadow (it is flush with the
 * desktop, not floating above it), no grips (nothing left to resize), and a bar
 * that stops claiming to be a drag handle. The box itself is written by the
 * window manager into the desktop area, so nothing here sets a position or a
 * size — that is how the taskbar stays reachable. */

:is([data-window], .app).is-maximized {
  border-radius: 0;
  border-width: 0;
  box-shadow: none;
}
:is([data-window], .app).is-maximized .win__grip { display: none; }
:is([data-window], .app).is-maximized :is(.win__bar, .app__bar) {
  border-radius: 0;
  cursor: default;
}


/* ══ 390 ═══════════════════════════════════════════════════════════════════
 * Below 760px the desktop is a phone, and a phone has no room for a window that
 * is smaller than the screen. The window takes the whole area, loses its radius
 * for the same reason the maximised state does, and keeps every control — the
 * lights stay 15px because that is what a thumb needs. */

@media (max-width: 760px) {
  :is([data-window], .app) {
    inset: 0 !important;
    width: 100% !important;
    height: 100% !important;
    max-width: none;
    max-height: none;
    border-width: 0;
    border-radius: 0;
    box-shadow: none;
  }
  /* The ring is the whole focus signal on a phone, so it survives — drawn as an
     inset ring on the bar, where it cannot be clipped off-screen. */
  :is([data-window], .app).is-focused :is(.win__bar, .app__bar) {
    box-shadow: inset 0 3px 0 var(--win-focus);
  }
  .win__grip { display: none; }
  :is(.win__bar, .app__bar) { min-height: 3.25rem; }
  :is(.win__icon, .app__logo) { width: 24px; height: 24px; }
  :is(.win__title, .app__title) { font-size: 1rem; }
  :is(.win__sub, .app__trade) { font-size: .7rem; }
  .win__light { width: 17px; height: 17px; }
  .win__light:focus-visible { outline-offset: 4px; }
  /* no hover on a phone, so the affordance has to survive a tap */
  .win__light:active { background: var(--win-light-close); }
  :is(.win__actions, .app__wins) :is(button, a) { padding: .45rem .7rem; font-size: .78rem; }
}

/* 390px on its side: the bar cannot hold a title and two controls. The title is
   the one thing that may shrink, because the controls are the ones that have to
   be hit. */
@media (max-width: 760px) and (max-height: 560px) {
  :is(.win__sub, .app__trade) { display: none; }
  :is(.win__actions, .app__wins) :is(button, a) { padding: .5rem .8rem; }
}


/* ══ reduced motion ═════════════════════════════════════════════════════════
 * Correctness must not depend on JS setting an attribute, so the media query is
 * the authority and the data attribute is honoured as a mirror for whoever
 * prefers to set one. Everything here is decoration on top of state changes the
 * window manager makes synchronously, so all of it can go. The end states are
 * untouched: the window is still placed, focused, ringed and maximised. */

@media (prefers-reduced-motion: reduce) {
  [data-reduce-motion] :is([data-window], .app),
  [data-reduce-motion] .app,
  :is([data-window], .app),
  .app {
    animation: none !important;
    transition: none !important;
  }
  :is([data-window], .app).is-opening,
  :is([data-window], .app).is-closing { opacity: 1; scale: 1; translate: 0; }
  :is([data-window], .app).is-minimized { opacity: 0; }
}

/* The same guarantee for an OS that asks for it in the DOM instead of the
   media query, so a user-facing toggle works with the motion preference
   untouched. */
[data-reduce-motion] :is([data-window], .app),
[data-reduce-motion] .app {
  animation: none !important;
  transition: none !important;
}
[data-reduce-motion] :is([data-window], .app).is-opening,
[data-reduce-motion] :is([data-window], .app).is-closing { opacity: 1; scale: 1; translate: 0; }
[data-reduce-motion] :is([data-window], .app).is-minimized { opacity: 0; }


/* ══ forced colours ═════════════════════════════════════════════════════════
 * The shadows and the accent ring are the only things separating a focused
 * window from an unfocused one here, and a forced palette replaces both. So focus
   is drawn with an outline in forced colours, and the lights lose their fills and
   keep their outlines — the three shapes stay distinguishable. */

@media (forced-colors: active) {
  :is([data-window], .app) { border-color: CanvasText; border-width: 2px; }
  :is([data-window], .app).is-focused {
    outline: 3px solid Highlight;
    outline-offset: 2px;
    box-shadow: none;
  }
  :is([data-window], .app).is-maximized { border-width: 0; }
  :is([data-window], .app).is-maximized.is-focused { outline-offset: -3px; }
  .win__light { background: Canvas; border-color: CanvasText; }
  .win__light::before, .win__light::after { forced-color-adjust: none; }
  .win__light:focus-visible { outline: 3px solid Highlight; }
  .win__grip[data-edge]::after { opacity: .55 !important; }
}