/*
 * Profile / Card — laminate preset.
 *
 * A backstage AAA credential. The portrait is the card: it runs edge to edge and
 * takes every pixel the identity block does not need. A saturated spot band
 * crosses the lower area carrying the name in heavy condensed caps with the role
 * tracked beneath it, and the actions read as access rights in one compact row
 * rather than as a stack of ruled entries.
 *
 * Proportion is not fixed here the way identity-program fixes it. The band is
 * sized by its own type and the portrait takes the remainder, so a name that
 * wraps eats into the portrait instead of overflowing the card. Type is sized
 * from the card's own width, so "A. Reyes" and "Alessandra Villanueva-Restrepo"
 * carry the same authority.
 *
 * This preset is the platform's first demonstration that motion is possible: the
 * card sways as if hanging on a lanyard and a specular sheen travels across the
 * laminate. Both are CSS only — no script is required to read, save, or act on
 * this card — both are wrapped in prefers-reduced-motion, neither moves layout,
 * and neither can intercept a tap.
 */

:root {
  /*
   * A backstage credential is a dark object. The colour scheme adjusts its
   * depth, not its polarity: inverting the ground would make it a different
   * object, not the same one at night.
   */
  --stock: #101014;
  --ink: #e8e4dc;
  --spot: #cc4022;
  --steel: #8f9aa6;
  --hairline: rgba(232, 228, 220, 0.24);

  /* Bone on the spot band, always: the band is ink, not paper. */
  /*
   * The ink on the spot band is DERIVED, never assumed. A fixed bone reads at
   * 1.51:1 on Khaos gold — illegible — while measuring the Publisher's actual
   * colour picks near-black at 12.4:1. The renderer emits --spot-on; this is
   * only the fallback when the stylesheet is opened alone.
   */
  --band-ink: var(--spot-on, #e8e4dc);

  --rule-hair: 1px;

  /* One governing axis. The band, the register and the mark all pin to it. */
  --gutter: clamp(16px, 5cqw, 44px);

  /*
   * System condensed only. A credential is set in whatever narrow grotesque the
   * printer had; no webfont is fetched, so the card renders at first paint.
   */
  --face-display: "Arial Narrow", "Helvetica Neue Condensed", "Liberation Sans Narrow", "Roboto Condensed", ui-sans-serif, system-ui, sans-serif;
  --face-label: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
}

:root[data-scheme="dark"] {
  --stock: #08080b;
  --hairline: rgba(232, 228, 220, 0.18);
}

*,
*::before,
*::after { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--stock);
  color: var(--ink);
  /* The card is one screen and never scrolls; the sway must not be able to
     produce a scrollbar by swinging its bounding box past the viewport. */
  overflow: hidden;
}

/*
 * The depth the turn is seen in.
 *
 * `perspective` applies to an element's CHILDREN, and the elements that rotate
 * are the faces — so it belongs on the card, which is their parent. It stood on
 * `body`, whose only child here is the card, and the card's own transform is
 * two-dimensional: the property had nothing to act on and the faces turned in
 * an orthographic projection.
 *
 * The distance was also wrong, and that is what the reader actually saw.
 * Measured on the turning face against the flat cosine an orthographic squash
 * would give, at 40 and 60 degrees:
 *
 *     none      -3.0   -1.9      (a rectangle narrowing to a slit)
 *     1600px    -0.4   +1.1      what shipped: one pixel of depth
 *     1100px    +2.5   +4.7
 *      820px    +7.0  +10.2
 *      620px   +14.9  +20.2
 *
 * 1600px against a card 439px wide is a long lens on a postcard: geometrically
 * a perspective, optically none, so the card folded flat instead of turning
 * over.
 *
 * 1100px is the choice, and the bound is the room rather than the drama. The
 * near half of a turning card is scaled by d / (d - z), and at the steepest
 * part of the turn z is about 180px: 1.20 at 1100px, 1.28 at 820px, 1.41 at
 * 620px. Against a 653px card in an 813px window only the first keeps the
 * object inside the room — 620px threw its top edge clean out of frame. The
 * near edge still renders about 39% larger than the far one, which is the whole
 * difference between a card turning and a page folding.
 */

/*
 * The card is the holder: the object that hangs, sways and turns over. It owns
 * no measure and clips nothing, because it can afford neither — layout
 * containment and any overflow other than `visible` both force
 * `transform-style` back to `flat`, and a flat card has no two sides. So the
 * container and the clipping moved down onto the faces, which need them, and
 * the card kept only the geometry, the pivot and the depth.
 */
.card {
  position: relative;
  perspective: 1100px;
  height: 100svh;
  min-height: 100svh;
  margin-inline: auto;
  width: 100%;
  /*
   * No background on the holder. Each face stands on the stock itself, and a
   * background HERE does not turn — it is painted by the box the faces rotate
   * inside. Through the turn it stood behind them as a flat dark slab the size
   * of the card, occupying exactly the space a turning card is supposed to be
   * leaving. A private card has no faces and keeps its own stock.
   */
  transform-style: preserve-3d;
  /* Where a lanyard clip would be. The card never scales — scaling it would
     make it taller than its own screen and push an action out of reach. */
  transform-origin: 50% -8%;
}

/*
 * The two sides of one card.
 *
 * Each face is the surface the old `.card` was: it carries the container so
 * every cqw measure below resolves against the same inline size it always did,
 * it clips its own contents, and it stands on the credential's stock so a
 * half-turned card never shows through to the ground.
 *
 * `visibility` is what actually takes the hidden face out of the tab order and
 * out of the accessibility tree — `backface-visibility` only stops it being
 * DRAWN, and a keyboard landing on a control the reader cannot see is a defect
 * whichever way the card is facing. It is switched at the half-way point of the
 * turn, so the incoming side appears exactly as it comes round.
 */
.card__face {
  container-type: inline-size;
  position: absolute;
  inset: 0;
  overflow: hidden;
  background: var(--stock);
  border-radius: inherit;
  backface-visibility: hidden;
  transform-origin: 50% 50%;
}

.card__face--front { display: flex; flex-direction: column; }

/*
 * At rest the reverse is turned away. Each face carries its own rotation rather
 * than the card carrying one for both: the card's own `transform-origin` is the
 * lanyard clip, eight percent above its top edge, which is right for a sway and
 * wrong for a turn. Rotating the faces keeps each pivot where it belongs.
 */
.card__face--back {
  /* A size container, not just an inline one: the reverse is a single block of
     prose in a fixed box, so it has to be able to set itself from the box's
     HEIGHT as well as its width. The front face has a flow to yield to and
     needs no such measure. */
  container-type: size;
  transform: rotateY(-180deg);
  visibility: hidden;
}

/*
 * The turn itself: `:target`, set by an ordinary fragment link, reached from the
 * card by `:has()`. No script is involved and none is available.
 *
 * `:has()` is the same generation of CSS this stylesheet already depends on for
 * `color-mix()` and container queries — a browser without it cannot render this
 * preset's ink or its measures either. Where it is missing the selector is
 * dropped and the card stays face-up, which is exactly what it did before.
 */
.card:has(.card__face--back:target) .card__face--front {
  transform: rotateY(180deg);
  visibility: hidden;
}

.card:has(.card__face--back:target) .card__face--back {
  transform: rotateY(0deg);
  visibility: visible;
}

/*
 * The pouch turns with the card.
 *
 * It stayed still while the card inside it narrowed, so the badge read as a
 * page closing inside a fixed frame — the second half of the same fault as the
 * missing perspective. A real holder does turn; what stays true is that both
 * sides are seen through the same plastic, and they still are, because this is
 * one pouch rotating rather than two.
 *
 * The clip does not turn with it: it is hardware riveted at the top edge, and
 * it looks the same from either side.
 */
.card:has(.card__face--back:target)::after { transform: rotateY(180deg); }

/*
 * The specular sheen. It sits above everything so the laminate reads as one
 * glossy surface rather than a gloss on the portrait only, and it is inert to
 * the pointer so it can never take a tap that belongs to an action. At rest it
 * is a fixed highlight near the left edge; motion only moves it.
 *
 * On the face rather than on the card, so the reverse is laminated too: the
 * gloss is a property of the pouch, and a back with no sheen would read as a
 * different material the moment the card came round.
 */
.card__face::after {
  content: "";
  position: absolute;
  inset: -25% -60%;
  z-index: 4;
  pointer-events: none;
  mix-blend-mode: screen;
  background: linear-gradient(
    102deg,
    transparent 44%,
    rgba(232, 228, 220, 0.05) 47%,
    rgba(232, 228, 220, 0.14) 50%,
    rgba(232, 228, 220, 0.04) 53%,
    transparent 56%
  );
  transform: translateX(-16%);
}

/* ---- portrait ------------------------------------------------------------ */

/*
 * The dominant element: it takes every pixel the identity block leaves. A long
 * name that wraps shrinks the portrait; it never pushes the register off-screen.
 */
.card__portrait {
  position: relative;
  z-index: 1;
  flex: 1 1 auto;
  min-height: 0;
}

.card__portrait img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 26%;
}

/*
 * No portrait: the unphotographed credential.
 *
 * A credential that has been manufactured and printed and whose photograph has
 * not been taken yet is a real object in the world, which is why this does not
 * read as an error. This preset used to refuse such a record outright; that was
 * wrong, and no design refuses a Publisher for not having uploaded a picture.
 *
 * What fills the frame is a photograph that ships with the renderer: a security
 * -camera bust, the figure present but the face destroyed into datamosh banding
 * and chroma smear. It is a real image in a preset made of photographic
 * material, so it is an <img> in the portrait's own frame and every rule above
 * applies to it unchanged — the cover crop, the `center 26%` head position, the
 * fall-to-ground seam, the veiled bleed, and the parallax drift. There is no
 * second rule set to keep in step, which is the whole reason it is an image.
 *
 * It is the SAME picture on every laminate card. That is the cost, it was
 * accepted deliberately, and `docs/PROFILE-CARD-PORTRAIT.md` §0 records it.
 */
.card__portrait--unset {
  /*
   * The credential's own stock, so a frame whose image has not arrived — or
   * cannot — is dark laminate rather than a hole. Nothing else is declared:
   * every measure the picture needs is already on `.card__portrait img`.
   */
  background: var(--stock);
}

/* The portrait meets the band through a short fall to the ground, not a seam. */
.card__portrait::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  height: 22%;
  background: linear-gradient(to bottom, transparent, color-mix(in srgb, var(--stock) 72%, transparent));
}

/*
 * The same asset, blurred, as the ground the credential is laminated over. One
 * asset used twice; heavily veiled, because on this preset it is atmosphere
 * behind a dark card rather than a second image to read.
 */
.card__bleed {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
}

.card__bleed img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: blur(52px) saturate(1.2);
  transform: scale(1.3);
}

.card__bleed::after {
  content: "";
  position: absolute;
  inset: 0;
  background: color-mix(in srgb, var(--stock) 84%, transparent);
}

/* ---- identity ------------------------------------------------------------ */

.card__body {
  position: relative;
  z-index: 2;
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
}

/*
 * The band. Name and role share one saturated field that bleeds to both edges;
 * only the type is inset to the gutter. The name is set from container width and
 * wraps rather than overflows, so the band grows by a line instead of clipping.
 */
.card__name {
  margin: 0;
  padding: clamp(10px, 2.6cqw, 26px) var(--gutter) clamp(2px, 0.6cqw, 6px);
  background: var(--spot);
  border-top: 2px solid rgba(8, 8, 11, 0.5);
  color: var(--band-ink);
  font-family: var(--face-display);
  font-weight: 700;
  font-stretch: condensed;
  font-size: clamp(2rem, 13.5cqw, 7rem);
  line-height: 0.9;
  letter-spacing: 0.005em;
  text-transform: uppercase;
  overflow-wrap: anywhere;
}

/* On a credential the initial is not a drop cap; the whole name is one stamp. */
.card__initial,
.card__surname { color: inherit; }

.card__role {
  display: flex;
  align-items: center;
  gap: var(--gutter);
  margin: 0;
  padding: clamp(4px, 1cqw, 10px) var(--gutter) clamp(9px, 2.2cqw, 20px);
  background: var(--spot);
  color: color-mix(in srgb, var(--band-ink) 90%, var(--spot));
  font-family: var(--face-label);
  font-size: clamp(0.62rem, 2.5cqw, 1.05rem);
  font-weight: 700;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  overflow-wrap: anywhere;
}

/* The clearance mark: a punched square at the end of the role line. */
.card__role-mark {
  flex: none;
  margin-inline-start: auto;
  width: 0.8em;
  height: 0.8em;
  background: var(--band-ink);
}

/*
 * The issuing line: who the credential is for and by what name they are
 * addressed. It sits BELOW the band, on the stock, because it is not part of the
 * stamp — a real credential prints the holder's title in the field and the
 * issuing body in small type beneath it.
 *
 * The separator is drawn, not typed. It appears only when both parts are
 * present, so a Publisher with an organization and no pronouns gets no dangling
 * mark and a screen reader is never read a punctuation character.
 */
.card__affiliation {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.7em;
  margin: 0;
  padding: clamp(6px, 1.4cqw, 12px) var(--gutter) 0;
  color: var(--steel);
  font-family: var(--face-label);
  font-size: clamp(0.6rem, 2.3cqw, 0.95rem);
  font-weight: 700;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  overflow-wrap: anywhere;
}

.card__organization + .card__pronouns::before {
  content: "";
  display: inline-block;
  margin-inline-end: 0.7em;
  width: 0.34em;
  height: 0.34em;
  background: currentcolor;
  vertical-align: 0.22em;
}

/* ---- credential furniture ------------------------------------------------ */

/*
 * The micro-caps header printed across the top of the card, over the portrait:
 * "PROPERTY OF BEARER" at the left, "NON-TRANSFERABLE" at the right.
 *
 * It is CSS content and not markup because it is costume, not record: it says
 * nothing about the Publisher, it is the same on every card, and putting it in
 * the renderer would mean the renderer knowing which preset it was drawing.
 *
 * It IS announced. Generated content reaches the accessibility tree in every
 * current engine — measured in the rendered page, where "PROPERTY OF BEARER
 * NON-TRANSFERABLE" appears as static text ahead of the name — and there is no
 * way to mark a pseudo-element hidden. That is accepted rather than worked
 * around: the words are printed on the card and a sighted reader reads them
 * too, so suppressing them would hand the two readers different cards. The
 * thing that would be wrong is putting information here that is only available
 * to one of them, and none of this is information.
 *
 * The two clusters are pushed to opposite ends by justifying a single line:
 * every space INSIDE a phrase is non-breaking, so the line has exactly one
 * breaking space and `text-align-last: justify` opens that one to the full
 * measure. One pseudo-element, two apparent columns, no extra box.
 */
.card__portrait::before {
  /*
   * The space after each escape is REQUIRED and is not itself rendered: a CSS
   * unicode escape consumes up to six hex digits, so `\00a0BEARER` is read as
   * U+00A0BE followed by "ARER" — which is exactly what the card printed until
   * the terminating space was added. The space ends the escape and is consumed
   * with it.
   */
  content: "PROPERTY\00a0 OF\00a0 BEARER NON-TRANSFERABLE";
  position: absolute;
  z-index: 3;
  inset-inline: var(--gutter);
  /* Clear of the sleeve's top seal and its slot punch, which cross the card's
     first few millimetres. Type under a seam is type nobody can read. */
  top: clamp(26px, 8.5cqw, 58px);
  text-align: justify;
  text-align-last: justify;
  color: color-mix(in srgb, var(--ink) 78%, transparent);
  font-family: var(--face-label);
  font-size: clamp(0.44rem, 1.85cqw, 0.78rem);
  font-weight: 700;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.85);
  pointer-events: none;
}

/*
 * The holographic seal, applied over the portrait's lower right the way a foil
 * sticker is applied to a finished credential — after everything else, slightly
 * off the grid, overlapping the band's edge.
 *
 * A background image on the portrait's own frame: it is pure decoration with no
 * information in it, so it needs no element and no alternative text. The file
 * is a photograph of iridescence; nothing in CSS makes that.
 */
.card__body::before {
  content: "";
  position: absolute;
  z-index: 3;
  right: var(--gutter);
  bottom: 100%;
  /* Clear of the band: on the reference the seal is wholly on the portrait, and
     one that straddles the edge reads as a sticker applied crookedly. */
  margin-bottom: clamp(6px, 1.6cqw, 16px);
  width: clamp(52px, 21cqw, 112px);
  aspect-ratio: 1;
  background: url("seal.png") center / contain no-repeat;
  filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.6));
  pointer-events: none;
}

/* ---- the turn ------------------------------------------------------------ */

/*
 * The control that turns the card over. A link, not a button: it is a fragment
 * link and `:target` is the mechanism, so a keyboard, a screen reader and a
 * touch screen all already know what it is.
 *
 * It is quiet on purpose. Saving the contact is why the page exists and stays
 * the loudest thing in the identity block; reading the reverse is an invitation.
 */
.card__turn {
  display: inline-flex;
  align-items: center;
  justify-content: flex-end;
  gap: 0.7em;
  /*
   * Taken out of the column and set against the fine print, which is short
   * enough to sit beside it. In flow it claimed a full 44px row of its own, and
   * every one of those pixels came off the portrait — the card's dominant
   * element and the one thing on it that is actually the person.
   *
   * It keeps the full 44px target; the target simply overlaps type it does not
   * collide with rather than pushing it.
   */
  position: absolute;
  right: var(--gutter);
  bottom: clamp(8px, 1.8cqw, 16px);
  min-width: 44px;
  min-height: 44px;
  color: var(--steel);
  font-family: var(--face-label);
  font-size: clamp(0.62rem, 2.4cqw, 0.98rem);
  font-weight: 700;
  letter-spacing: 0.22em;
  text-decoration: none;
  text-transform: uppercase;
}

/* The arrow is a shape, not a glyph, so no font has to carry it. */
.card__turn-mark {
  flex: none;
  width: 0.5em;
  height: 0.62em;
  background: var(--spot);
  clip-path: polygon(0 0, 100% 50%, 0 100%);
}

.card__turn--return .card__turn-mark { scale: -1 1; }

.card__turn:hover { color: var(--ink); }

.card__turn:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
  color: var(--ink);
}

/* ---- the reverse --------------------------------------------------------- */

/*
 * The back of the credential. No portrait, no band, no register: a real badge
 * prints its terms on the reverse in one block of small type, and this one
 * prints the Publisher's own paragraph there instead.
 *
 * Set in the display face rather than the mono label, because this is the only
 * running prose on the card and the label face is the operational voice — a
 * biography tracked at 0.22em would be a specification, not a sentence.
 */
.card__reverse {
  position: relative;
  z-index: 2;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: clamp(14px, 3cqw, 28px);
  height: 100%;
  padding: var(--gutter);
}

/*
 * The spot, spent once on the reverse: a short rule above the paragraph, the
 * mark of the issuing authority. It costs no colour anywhere else on this face.
 */
.card__reverse::before {
  content: "";
  flex: none;
  width: clamp(38px, 12cqw, 84px);
  height: 3px;
  background: var(--spot);
}

.card__biography {
  margin: 0;
  color: var(--ink);
  font-family: var(--face-display);
  /* Bounded by the face's height as well as its width: the contract admits 280
     characters and the card never scrolls, so a long paragraph in a short window
     has to set smaller rather than run past the edge. */
  font-size: clamp(0.92rem, min(6.4cqw, 5.2cqh), 1.9rem);
  font-weight: 400;
  line-height: 1.34;
  letter-spacing: 0.004em;
  overflow-wrap: anywhere;
}

/* ---- access rights ------------------------------------------------------- */

/*
 * Not a stack of ruled entries. One compact row of rights, divided by hairlines,
 * that wraps to a second row before it ever shrinks below a 44px target.
 */
.card__register {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  margin: clamp(7px, 1.6cqw, 14px) var(--gutter) 0;
  padding: 0;
  border-top: var(--rule-hair) solid var(--hairline);
  list-style: none;
}

.card__register > li { display: flex; }

.card__entry {
  display: flex;
  align-items: center;
  gap: 0.5em;
  min-height: 44px;
  padding-block: clamp(7px, 1.6cqw, 14px);
  padding-inline-end: clamp(10px, 2.6cqw, 22px);
  color: var(--steel);
  font-family: var(--face-label);
  font-size: clamp(0.68rem, 2.7cqw, 1.05rem);
  font-weight: 700;
  letter-spacing: 0.22em;
  text-decoration: none;
  text-transform: uppercase;
}

/*
 * The hairline TRAILS each right rather than leading it, and the last one has
 * none. That is what keeps a wrapped row flush at the gutter.
 *
 * It used to lead, with the first item's border and padding removed. That
 * worked for the first item in the MARKUP and for no other: once the row
 * wrapped — which it does at the badge's fixed width, and did on every phone
 * before that — the second row began with a divider and a left inset, and sat
 * visibly indented from the gutter every other line pins to. The comment here
 * claimed the flush behaviour the CSS did not implement.
 *
 * The cost is a trailing hairline at the end of a row that wrapped. That is a
 * register continuing, which is what it is, and it is a far quieter artefact
 * than an indent.
 */
.card__register > li:last-child .card__entry { padding-inline-end: 0; }

.card__register > li:not(:last-child) .card__entry {
  border-inline-end: var(--rule-hair) solid var(--hairline);
  margin-inline-end: clamp(10px, 2.6cqw, 22px);
}

/*
 * The primary is the highest clearance in the row, not a button: bone instead of
 * steel, condensed instead of mono, and stamped with the spot square.
 */
.card__entry--primary {
  color: var(--ink);
  font-family: var(--face-display);
  font-weight: 700;
  font-stretch: condensed;
  font-size: clamp(1rem, 4.4cqw, 1.85rem);
  letter-spacing: 0.06em;
}

.card__entry--primary::before {
  content: "";
  flex: none;
  width: 0.72em;
  height: 0.72em;
  background: var(--spot);
}

.card__entry:hover { color: var(--ink); background: color-mix(in srgb, var(--ink) 8%, transparent); }

/*
 * Bone, not the spot: the focus ring must stay visible whatever spot colour the
 * Publisher chose, and bone against the near-black ground is the one contrast
 * this preset can guarantee.
 */
.card__entry:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: -2px;
  color: var(--ink);
  background: color-mix(in srgb, var(--ink) 8%, transparent);
}

.card__index { display: none; }

/* ---- the footer ---------------------------------------------------------- */

/*
 * The access line. Costume, like the header: it names the clearance the object
 * pretends to grant, it is identical on every card, and it says nothing about
 * the Publisher — so it is generated content and never markup.
 */
.card__name { order: 1; }
.card__role { order: 2; }
.card__register { order: 3; }
.card__affiliation { order: 4; }
.card__turn { order: 6; }
.card__mark { order: 7; }

/* ---- mark ---------------------------------------------------------------- */

.card__mark {
  padding: clamp(9px, 2cqw, 18px) var(--gutter) clamp(10px, 2.4cqw, 22px);
}

.card__mark img {
  display: block;
  width: clamp(18px, 4.4cqw, 30px);
  height: auto;
  opacity: 0.72;
}

/* ---- unpublished --------------------------------------------------------- */

/*
 * A private card has one side and nothing to turn to, so it carries the
 * container the faces would otherwise have carried: its own measures are still
 * written in cqw and there is no face below it to resolve them against.
 */
.card--private {
  background: var(--stock);
  container-type: inline-size;
  display: grid;
  place-items: center;
  padding: var(--gutter);
  font-family: var(--face-label);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--steel);
  text-align: center;
}

/* ---- corners ------------------------------------------------------------- */

:root[data-corners="soft"] .card { border-radius: 12px; }

/* ---- desktop framing ----------------------------------------------------- */

/*
 * A credential is a manufactured object with a size. On a monitor this card had
 * neither: `width: 100%` against `height: 100svh` made it a 16:9 banner, and
 * the portrait — which takes every pixel the band leaves — cropped the subject
 * to a horizontal sliver under `object-position: center 26%`.
 *
 * So the card is held to 2:3, the proportion of the shipped placeholder
 * (700x1050) and of identity-program's comp, and centred on a ground. This is
 * the preset that most needs it: everything the record claims for laminate — it
 * hangs, it sways, light crosses it — describes an object seen against
 * something, and a card bleeding to all four edges of a monitor is a wall, not
 * a credential. The sway only became legible once there was ground to sway
 * against.
 *
 * The condition is the window's proportion rather than its width, and at
 * exactly 2/3 the two branches agree, so the card grows into its frame with no
 * snap. Everything below is declared inside the query: a phone matches nothing
 * here and renders exactly the cascade it always did.
 */
@media (min-aspect-ratio: 2 / 3) {
  :root {
    /*
     * The room it hangs in, not a surface it was placed on — so darker than the
     * credential's own stock. Tone alone carries the card's edge, and it is
     * enough: `.card__bleed` lifts the whole card above its declared --stock,
     * measured at roughly #26262b against this ground even in the register,
     * which is the darkest place the two meet. A hairline outline was tried
     * here for the sealed border of a real pouch and removed — magnified, it
     * changed nothing an eye could find, and the specular sheen already
     * supplies the edge light it was standing in for.
     */
    --ground: #050507;

    /*
     * THE BADGE HAS A SIZE.
     *
     * What stood here was `width: calc(100svh * 2 / 3)` — the card held to the
     * right proportion but still measured from the window, so it grew without
     * limit as the window did. That fixed the crop it was written to fix and
     * left the object wrong: a credential enlarged to eighteen inches is a
     * poster of a credential. The operator asked for the badge itself, and a
     * manufactured object does not change size when you widen the room.
     *
     * So the measure is a length, not a fraction of anything. 720px is the
     * operator's ruling: 560px read as a correct object and a small one on a
     * desk-sized display, so the badge is set at the size it reads at desk
     * distance while staying a fixed length rather than a fraction.
     *
     * `min()` here is a CAP and not a scale. On any window tall enough it
     * resolves to the constant and the badge is genuinely fixed; the second and
     * third terms only ever engage on a window too short to hold the object,
     * where the alternative is not a bigger badge but a badge with its register
     * cut off by the bottom of the screen. A landscape phone is that case.
     */
    --badge-height: min(720px, calc(100svh - 160px));
    --badge-width: calc(var(--badge-height) * 2 / 3);

    /*
     * How far the badge hangs below the top of the room. The strap has to reach
     * the EDGE of the frame — a lanyard comes from around a neck that is not in
     * the picture, and one that starts in mid-air with two cut ends is a ribbon.
     * So the assembly is anchored to the top and this length, not a centring
     * rule, is what places the badge.
     *
     * It takes a share of whatever room is left over, with a floor, so a tall
     * window lengthens the strap instead of stranding the badge under the
     * ceiling. The badge itself does not change size either way — the strap is
     * the only thing here that stretches, which is also true of the real object.
     */
    --lanyard-drop: max(90px, calc((100svh - var(--badge-height)) * 0.42));

    /*
     * The plastic's inner edge.
     *
     * The pouch is drawn 7px proud of the card, and its lip and sealed edge
     * occupy the outer 2.3% of the artwork — measured on the file, not guessed:
     * a bright line at the very edge and a second at 21px of 718. Stretched to
     * this card that is about 15px, so anything the card sets within that band
     * is read through the fold of the plastic. Type must clear it; the portrait
     * and the spot band must NOT, because a card inside a pouch does run under
     * the lip.
     */
    --lip: calc(var(--badge-height) * 0.022);

    /* The strap's width. The artwork is a tile; this is how wide it hangs. */
    --webbing: clamp(26px, calc(var(--badge-width) * 0.13), 46px);
    /* How far the clip straddles the badge's top edge. */
    --hardware: clamp(46px, calc(var(--badge-width) * 0.26), 92px);
  }

  :root[data-scheme="dark"] { --ground: #000000; }

  html, body { background: var(--ground); }

  /*
   * The assembly hangs from the top of the room: the strap, then the badge. It
   * is centred as a whole, so the badge sits a little below the optical centre
   * — which is where a thing that hangs actually sits.
   */
  body {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    height: 100svh;
  }

  /*
   * The lanyard: a photograph of woven polyester webbing, tiled down its own
   * length. The artwork is a strap and nothing else — cropped to the weave, so
   * the tile repeats without a seam and the strap can be any length the room
   * needs. Nothing here approximates fabric; the fabric is the file.
   *
   * It is a pseudo-element of the ROOM and not of the card, so it does not turn
   * when the card turns and does not swing when the card swings. That is the
   * correct physics: the strap is what the badge hangs from, and the clip is the
   * pivot the sway already rotates about.
   *
   * A privacy response hangs from nothing, so the selector asks what is in the
   * room before furnishing it. That is not decoration: `render.mjs` ships a
   * preset's chrome only for a PUBLISHED card, so a private release does not
   * contain `lanyard.jpg` at all, and a `content: ""` box with a background
   * image issues a request whether or not anyone can see it. Measured before
   * this scope existed: a private card fetched `lanyard.jpg` and would have
   * taken a 404 on every view of the state most Profiles are in.
   */
  body:not(:has(.card--private))::before {
    content: "";
    flex: none;
    width: var(--webbing);
    /* Overlaps the badge's top edge so the strap disappears behind the clip
       rather than stopping short of it. */
    height: calc(var(--lanyard-drop) + 14px);
    margin-bottom: -14px;
    /*
     * The strap takes the Publisher's colour, exactly as the band does. The
     * artwork ships desaturated and the spot is multiplied THROUGH the weave, so
     * the fabric keeps its own light and shadow while the dye is theirs. A fixed
     * red strap would have put a second colour on an object allowed one — a
     * Publisher whose spot is gold was getting a gold band on a red lanyard.
     */
    background-color: var(--spot);
    background-image: url("lanyard.jpg");
    background-position: top center;
    background-size: 100% auto;
    background-repeat: repeat-y;
    background-blend-mode: multiply;
  }

  /*
   * The badge in its holder. Height-driven and centred by the flex column above;
   * `margin-inline: auto` in the base cascade is now redundant and harmless.
   *
   * The shadow is what the earlier framing had no need of — a card that bled to
   * every edge was not in front of anything. This one is.
   */
  /*
   * The card's own fine print stops at the plastic. The band and the portrait
   * still bleed past it, which is what being inside a pouch looks like.
   */
  .card:not(.card--private) .card__body { padding-bottom: var(--lip); }

  .card:not(.card--private) .card__turn {
    bottom: calc(var(--lip) + clamp(8px, 1.8cqw, 16px));
  }

  .card {
    width: var(--badge-width);
    height: var(--badge-height);
    min-height: 0;
    border-radius: 10px;
    /*
     * A shadow, and specifically NOT `filter: drop-shadow()`. A used filter
     * forces `transform-style` back to `flat`, exactly as overflow and layout
     * containment do, and a flat card has no second side to turn to. The whole
     * reason the container and the clipping moved down onto the faces was to
     * keep this element three-dimensional; a drop shadow would have undone it
     * silently, and the only symptom would have been a flip that stopped
     * working.
     */
    box-shadow: 0 26px 44px -12px rgba(0, 0, 0, 0.85);
  }

  /*
   * The sleeve: the clear pouch the card sits in.
   *
   * A photograph of an empty badge holder, reduced to only the light the
   * plastic ADDS — the rounded lip, the sealed edge, the diagonal gloss across
   * the upper left, the scuffs, and the slot punched near the top. Everything
   * else in that photograph is black, so `screen` composites it as pure
   * addition: the card shows through unchanged and the plastic sits over it.
   * That is also why it needs no alpha channel and why a JPEG's noise would be
   * visible — it ships as a one-channel PNG.
   *
   * It is inset NEGATIVELY, so the pouch is a few pixels proud of the card on
   * every side, which is what makes it read as something the card is inside
   * rather than a texture printed on it.
   *
   * On the card and not on a face: the pouch does not turn over when the card
   * inside it does. Both sides are seen through the same plastic.
   */
  .card:not(.card--private)::after {
    content: "";
    position: absolute;
    z-index: 6;
    inset: -7px;
    pointer-events: none;
    mix-blend-mode: screen;
    background: url("sleeve.png") center / 100% 100% no-repeat;
  }

  /*
   * The clip: the brushed metal bail the strap threads through, straddling the
   * top edge of the holder. Photographed hardware, keyed out of its background,
   * so the bevel and the brushing are real and not a stack of gradients.
   *
   * It belongs to the badge rather than to the room — it is riveted to the
   * pouch — so it sways with the card. It does not turn with it, because it is
   * a pseudo-element of the card and only the faces rotate.
   */
  .card:not(.card--private)::before {
    content: "";
    position: absolute;
    z-index: 7;
    left: 50%;
    bottom: 100%;
    translate: -50% 0;
    margin-bottom: -12px;
    width: var(--hardware);
    aspect-ratio: 747 / 407;
    background: url("clip.png") center / contain no-repeat;
    filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.75));
  }

  :root[data-corners="soft"] .card { border-radius: 16px; }
}

/* ---- motion -------------------------------------------------------------- */

/*
 * What a real laminate does: it hangs, so it sways; it is glossy, so the light
 * crosses it. Nothing else moves.
 *
 * The sway is a rotation about a point above the card — where a lanyard clip
 * would be — of a third of a degree, held to a slow, uneven cycle so it reads as
 * physical rather than mechanical. It transforms; it never reflows, so no
 * measure, line break, or hit target changes while it runs. The sheen is the
 * card's own ::after translating across; it is pointer-events: none, blends at a
 * peak alpha of 0.14, and pauses off-card for most of its cycle so it passes
 * over the type rather than sitting on it.
 *
 * Both live inside prefers-reduced-motion: no-preference, so a reader who has
 * asked for stillness gets the card's rest state and nothing is merely paused.
 */
@media (prefers-reduced-motion: no-preference) {
  .card:not(.card--private) {
    animation: laminate-sway 11s ease-in-out infinite;
  }

  /*
   * The sheen travels across the FACE, not across the card. It moved when the
   * card gained two sides: `.card::after` is now the punched slot the clip grips
   * and animating that would slide the hole across the badge. One selector
   * covers both sides, so the reverse is as glossy as the front.
   */
  .card:not(.card--private) .card__face::after {
    animation: laminate-sheen 14s cubic-bezier(0.4, 0, 0.2, 1) infinite;
  }

  /*
   * The turn. Declared here and nowhere else, so a reader who asked for
   * stillness gets an instantaneous change of side rather than a paused one —
   * the card still turns, it simply does not take time doing it.
   *
   * `visibility` switches at the half-way point, as the incoming side comes
   * round. Without the delay the outgoing face would vanish before it had
   * turned and the reader would watch an empty rectangle rotate.
   */
  .card__face {
    transition: transform 820ms cubic-bezier(0.4, 0, 0.2, 1), visibility 0s linear 410ms;
  }

  /* The pouch is part of the same object and takes the same time. */
  .card:not(.card--private)::after {
    transition: transform 820ms cubic-bezier(0.4, 0, 0.2, 1);
  }

  /*
   * The scale keeps the drifting portrait's edges outside the frame, so the
   * parallax never exposes the ground behind it.
   *
   * One selector covers both states because the placeholder is an <img> in the
   * same frame. The depth is this preset's whole illusion, and a plate that was
   * some other element would lose it silently — the card would just go flat on
   * every Publisher who has not uploaded a picture, and nothing would fail.
   */
  .card:not(.card--private) .card__portrait img {
    animation: laminate-portrait-drift 11s ease-in-out infinite;
    will-change: transform;
  }

  .card:not(.card--private) .card__body {
    animation: laminate-foreground-lift 11s ease-in-out infinite;
    will-change: transform;
  }
}

@keyframes laminate-sway {
  0%   { transform: rotate(-0.34deg); }
  38%  { transform: rotate(0.3deg); }
  64%  { transform: rotate(-0.16deg); }
  100% { transform: rotate(-0.34deg); }
}

/*
 * Depth, from the same motion.
 *
 * The card sways as one object, and inside it the portrait and the foreground
 * travel at slightly different rates. That difference is the whole effect: a
 * layer that moves less than the picture behind it reads as sitting ABOVE the
 * picture, the way a HUD sits over what it annotates, rather than being printed
 * on it. A few pixels of disagreement is enough — more would read as a glitch.
 *
 * Both are transform-only, so nothing reflows and no hit target moves.
 */
@keyframes laminate-portrait-drift {
  0%   { transform: translate3d(-0.5%, 0, 0) scale(1.06); }
  38%  { transform: translate3d(0.5%, -0.4%, 0) scale(1.06); }
  64%  { transform: translate3d(-0.2%, 0.2%, 0) scale(1.06); }
  100% { transform: translate3d(-0.5%, 0, 0) scale(1.06); }
}

@keyframes laminate-foreground-lift {
  0%   { transform: translate3d(0.18%, 0, 0); }
  38%  { transform: translate3d(-0.18%, 0.14%, 0); }
  64%  { transform: translate3d(0.07%, -0.07%, 0); }
  100% { transform: translate3d(0.18%, 0, 0); }
}

/*
 * Both ends of the travel sit clear of the card's own edges, so the loop's reset
 * happens off-screen: the highlight crosses once and returns unseen.
 */
@keyframes laminate-sheen {
  0%   { transform: translateX(-42%); }
  20%  { transform: translateX(-42%); }
  56%  { transform: translateX(42%); }
  100% { transform: translateX(42%); }
}

@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; transition: none !important; }
}

:root{--spot:#d8b928;--spot-on:#121210;--spot-text:#d8b928}
.card__portrait img{object-position:50% 34%}