/* ════════════════════════════════════════════════════════════════════════
   css/work-software.css — additions for /work/software ONLY.
   service.tnaado.ca · Tier 2, no storm · loaded AFTER css/work.css.

   WHY A SECOND SHEET. css/work.css is shared with /work and is owned
   elsewhere. Everything this page needs that /work does not — the grouped
   systems register, its disclosure panels, and the scroll reveal — lives
   here and is linked from this page alone.

   TOKENS ONLY. Every colour, font and easing below is a variable declared in
   css/work.css :root. Nothing new is invented; there is no rgba(bone,.4x)
   anywhere, per that sheet's contract.

   DEGRADATION, WHICH IS THE WHOLE POINT OF THIS PAGE.
   Nothing here hides content when script is off. The reveal is gated behind
   html.sw-anim, a class this page's own script adds to <html> before the
   first paint it can affect — so with no JavaScript there is no start state
   to be stuck in and every system, sentence and screen paints in flow. The
   disclosure panels are <details>, so they open with no script at all.
   prefers-reduced-motion is honoured here, in the stylesheet, so the script
   never needs to know about it.
   ════════════════════════════════════════════════════════════════════════ */

/* ── THE SYSTEMS REGISTER ─────────────────────────────────────────────────
   Replaces the flat nineteen-row list. Four named families, each a block, so
   the reader meets "what kinds of software the firm runs" before meeting
   nineteen product names in an arbitrary order. */

.sysfam {
  margin: 3rem 0 0;
  padding: 0;
  border: 0;
}
.sysfam:first-of-type { margin-top: 2.25rem; }

.sysfam__hd {
  display: flex;
  align-items: baseline;
  gap: .9rem;
  margin: 0 0 .25rem;
  padding: 0 0 .7rem;
  border-bottom: 1px solid var(--red);
  width: 100%;
}
.sysfam__name {
  font-family: var(--sans);
  font-size: .8125rem;
  font-weight: 600;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--bone);
}

.sysfam__list { list-style: none; margin: 0; padding: 0; }
.sysfam__list > li { margin: 0; border-bottom: 1px solid var(--hair-ink); }

/* ── ONE SYSTEM, AS A DISCLOSURE ──────────────────────────────────────────
   A bare <details>/<summary>: opens with no script, is keyboard operable by
   default, and the browser's own find-in-page opens a closed one to show a
   match. The panel content stays in the DOM at all times — on this page the
   screen IS the proof and it must stay indexable.

   NO hidden="until-found" ON THE PANEL. It was there, and it is a trap inside
   a <details>. The attribute is implemented as content-visibility: hidden, and
   NOTHING CLEARS IT when the details opens — the two features are independent.
   Verified at 1440 on 2026-08-21: every panel opened to a 22px sliver (its own
   padding-bottom, 1.4rem) with the figure clipped away, and because the image
   was never in a rendered box the lazy <img> never fired — currentSrc stayed
   empty on all fourteen screens. <details> already gives the find-in-page
   reveal on its own; adding the attribute only takes the content away. */

.sysd { display: block; }

.sysd__sum {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: start;
  gap: .5rem 1rem;
  padding: 1rem 0;
  cursor: pointer;
  list-style: none;
}
.sysd__sum::-webkit-details-marker { display: none; }
.sysd__sum::marker { content: ''; }

.sysd__name {
  grid-column: 1;
  font-family: var(--sans);
  font-size: .9375rem;
  font-weight: 500;
  color: var(--bone);
  line-height: 1.35;
}
.sysd__what {
  grid-column: 1;
  grid-row: 2;
  margin: 0;
  max-width: 62ch;
  font-size: .8125rem;
  line-height: 1.6;
  color: var(--dim-ink);
}

/* The affordance. A word, not an icon alone, because "there is more here" is
   not a thing a bare chevron reliably says. */
.sysd__open {
  grid-column: 2;
  grid-row: 1 / span 2;
  align-self: center;
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  font-family: var(--mono);
  font-size: .6875rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--red-ink);
  white-space: nowrap;
}
.sysd__chev {
  display: block;
  width: .5rem;
  height: .5rem;
  border-right: 1px solid currentColor;
  border-bottom: 1px solid currentColor;
  transform: rotate(45deg);
  transform-origin: 60% 60%;
  transition: transform .32s var(--ease);
}
.sysd[open] .sysd__chev { transform: rotate(-135deg); }

.sysd__sum:hover .sysd__name { color: var(--bone-2); }
.sysd__sum:hover .sysd__open { color: var(--bone); }

/* Focus ring on the row, not the text run, so keyboard position is obvious. */
.sysd__sum:focus-visible {
  outline: 2px solid var(--red-ink);
  outline-offset: 3px;
}

/* The label the open state swaps to. Both strings are in the DOM; CSS picks. */
.sysd__open .is-shut { display: inline; }
.sysd__open .is-open { display: none; }
.sysd[open] .sysd__open .is-shut { display: none; }
.sysd[open] .sysd__open .is-open { display: inline; }

/* ── THE PANEL ────────────────────────────────────────────────────────────
   Base behaviour: the browser reveals it instantly and the CONTENT fades and
   rises. No height measurement, so no script and nothing to get wrong at an
   awkward width. Where the engine supports it, the height animates too. */

.sysd__panel { padding: 0 0 1.4rem; }

.sysd__panel > * {
  opacity: 1;
  transform: none;
}

/* The real screen. Plate behind it so a slow or failed load is still a
   designed box rather than a collapse. */
/* ── THE UI-SCREENSHOT CEILING, WHERE IT ACTUALLY APPLIES ─────────────────
   css/site.css has carried a measured cap on these captures since T-1135 —
   max-height clamp(280px, 34vw, 440px) for img[src*="/gallery/systems/"] and
   img[src*="/apps/screens/"]. It has never done anything, because site.css is
   linked from /index alone and /index shows no system captures. Every page
   that DOES show one loads a different sheet, so the cap is restated here.

   AS A WIDTH, NOT A HEIGHT, and that is the whole difference. The site.css
   rule reaches 440px with `object-fit: cover`, which throws away the bottom
   third of a UI screenshot. These captures are 8:5, so 44rem of width lands on
   the same 440px with nothing cropped off. */
.sysd__shot {
  margin: 0;
  max-width: min(100%, 44rem);
  border: 1px solid var(--hair-ink);
  background: var(--plate-2);
}
.sysd__shot img {
  display: block;
  width: 100%;
  height: auto;
}
.sysd__cap {
  margin: 0;
  padding: .6rem .8rem;
  border-top: 1px solid var(--hair-ink);
  font-family: var(--mono);
  font-size: .6875rem;
  letter-spacing: .06em;
  color: var(--dim-ink);
}

/* SEVERAL SCREENS PER SYSTEM. One capture proved a system existed; it never
   showed what the system does, so a panel now holds the run of screens for
   that system and each one carries its own caption. The gap between two
   stacked captures is the panel's own bottom padding rather than a new value,
   and nothing loads until the row is opened, because a lazy image inside a
   closed <details> is never fetched. */
.sysd__panel > .sysd__shot + .sysd__shot { margin-top: 1.4rem; }

/* ── MOTION, ADDITIVE AND GATED ───────────────────────────────────────────
   html.sw-anim exists only when this page's script ran. */

.sw-anim .sysd[open] .sysd__panel > * {
  animation: swRise .46s var(--ease) both;
}
@keyframes swRise {
  from { opacity: 0; transform: translate3d(0, 10px, 0); }
  to   { opacity: 1; transform: none; }
}

/* NO HEIGHT ANIMATION ON THE PANEL, and this is a scar, not an omission.
   A ::details-content block was tried here:

     .sw-anim .sysd { interpolate-size: allow-keywords }
     .sw-anim .sysd::details-content { block-size: 0; overflow: hidden;
       transition: block-size .4s, content-visibility .4s allow-discrete }
     .sw-anim .sysd[open]::details-content { block-size: auto }

   Verified in a browser at 1440 on 2026-08-21: on an OPEN panel the computed
   block-size resolved to 22.4px, not the content height, with overflow hidden
   over it. So every disclosure clipped its screenshot down to a 22px sliver —
   and because the image was clipped out of the rendered box, the lazy <img>
   inside was never triggered at all: currentSrc stayed empty and the picture
   never loaded, on a page whose whole argument is that it shows you the
   screen rather than describing it.

   The content fade above is the motion. The box height snaps, which is what
   <details> does everywhere, and the screen is actually visible. A height
   animation is not worth risking the proof.

/* ── THE SCROLL REVEAL ────────────────────────────────────────────────────
   The repo's established Tier 2 pattern: a start state gated behind a class
   the script adds, .is-on written per element by an IntersectionObserver.
   Applied to the app cards, the client displays and the family blocks. */

.sw-anim [data-rise] {
  opacity: 0;
  transform: translate3d(0, 18px, 0);
  transition: opacity .62s var(--ease), transform .62s var(--ease);
  transition-delay: var(--rise-d, 0ms);
}
.sw-anim [data-rise].is-on {
  opacity: 1;
  transform: none;
}

/* ── THE STORE BADGE ──────────────────────────────────────────────────────
   Apple's artwork. Not recoloured, not redrawn, not cropped. The only thing
   this rule does is give it room and a focus ring: Apple's guidelines require
   clear space around the lockup and a minimum size, so there is deliberately
   no width/height override here — the intrinsic 120x40 stands. */
.storebadge {
  display: inline-block;
  margin: 1rem 0 0;
  padding: 0;
  line-height: 0;
  border-radius: 6px;
}
.storebadge img { display: block; }
.storebadge:focus-visible {
  outline: 2px solid var(--red-ink);
  outline-offset: 6px;   /* outside Apple's clear space, never inside it */
}
.sw-anim .storebadge {
  transition: opacity .28s var(--ease);
}
.sw-anim .storebadge:hover { opacity: .82; }

/* ── A COMPONENT MOUNT ────────────────────────────────────────────────────
   Empty until the ported showcase is available. Reserves no height and paints
   nothing, so an unfilled mount is invisible rather than a gap. */
[data-mount] { display: block; }
[data-mount]:empty { display: none; }

/* ── REDUCED MOTION ───────────────────────────────────────────────────────
   Everything above is decoration over content that is already in flow, so
   the honest response to the preference is to switch all of it off. */
@media (prefers-reduced-motion: reduce) {
  .sw-anim [data-rise],
  .sw-anim [data-rise].is-on {
    opacity: 1;
    transform: none;
    transition: none;
  }
  .sw-anim .sysd[open] .sysd__panel > * { animation: none; }
  .sysd__chev { transition: none; }
  .sw-anim .storebadge { transition: none; }
  @supports selector(::details-content) {
    .sw-anim .sysd::details-content { transition: none; }
  }
}

/* ── NARROW ───────────────────────────────────────────────────────────────
   At 390 the "See it" label is the first thing to go: the chevron alone is
   enough once the row is the full width of the screen and the whole row is
   the summary's hit area. */
@media (max-width: 560px) {
    .sysd__open .sysd__word { display: none; }
  .sysd__sum { padding: .9rem 0; gap: .4rem .75rem; }
  .sysd__panel { padding-bottom: 1.1rem; }
}

/* ── A SYSTEM WITH NOTHING TO DISCLOSE ────────────────────────────────────
   Five systems have no publishable product capture, so they are a plain row
   rather than a control that opens onto a paraphrase of its own summary.
   Same type, same rhythm, same sentence — the only difference a reader sees
   is that there is nothing further to press. Not a <details>, so there is no
   focusable control promising something that is not there. */
.sysd--flat .sysd__sum { cursor: default; }

/* ── THE JUMP INDEX ───────────────────────────────────────────────────────
   S.00 used to carry a sentence naming the three sections below it. This is
   that sentence turned into a control: same information, a third of the
   words, and it moves a reader eight thousand pixels down the page.

   Plain <a href="#…">. No script, no observer, no state — so it cannot be
   wrong about where the reader is, and it works with the page served as
   static HTML. The rule beneath each label is the affordance: it grows from
   the left on hover and on focus, which is the same gesture .tlink uses
   elsewhere on this page, so nothing new is being taught.

   TOKENS ONLY, per this sheet's contract. --red-ink is the page's only small
   red on ink; --dim-ink is its only dim; --ease is work.css's easing. */

.swjump {
  display: flex;
  flex-wrap: wrap;
  gap: .35rem 2.25rem;
  margin: 1.5rem 0 0;
  padding: 0;
}

.swjump__a {
  position: relative;
  display: inline-block;
  padding: .35rem 0 .6rem;
  color: var(--bone);
  font-family: var(--mono);
  font-size: .9375rem;
  letter-spacing: .01em;
  text-decoration: none;
}

/* The rule is a pseudo-element rather than a border so it can be scaled from
   0 without the label shifting by a pixel — transform only, no layout. */
.swjump__a::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: .15rem;
  height: 1px;
  background: var(--red-ink);
  transform: scaleX(0);
  transform-origin: 0 50%;
  transition: transform .32s var(--ease);
}

.swjump__a:hover::after,
.swjump__a:focus-visible::after { transform: scaleX(1); }

.swjump__a:hover { color: var(--bone); }

/* A visible focus ring is not optional: the underline alone is decoration and
   a keyboard user needs the box. Offset so it clears the rule. */
.swjump__a:focus-visible {
  outline: 2px solid var(--red-ink);
  outline-offset: 4px;
  border-radius: 2px;
}

/* The rule stops moving; the colour still answers. Reduced motion means no
   transform, not no feedback. */
@media (prefers-reduced-motion: reduce) {
  .swjump__a::after { transition: none; }
}

@media (max-width: 560px) {
  .swjump { gap: .1rem 1.5rem; }
}
