/* InviteRight — obsidian chrome, gold leaf, cream ink. The app is the case; the
   invitation is the thing inside it, and the cards stay bright on purpose.
   One accent (gold), used sparingly. Palette tokens live in ui.jsx's UI. */

* { margin: 0; }
html, body { padding: 0; }

body {
  min-height: 100vh;
  background: #FFFFFF;
  font-family: 'Jost', sans-serif;
  color: #2A2318;
  padding-bottom: 60px;
  position: relative;
  /* Two declarations on purpose. `hidden` stops sideways scrolling, but it
     also makes body a scroll container — one axis hidden forces the other to
     compute to `auto` — and `position: sticky` then sticks to THAT box, which
     never scrolls. That silently broke the customizer's sticky card preview:
     the class was applied, the rule matched, and the card simply sat still.
     `clip` clips exactly the same way without creating a scroll container, so
     sticky keeps working. Browsers without `clip` ignore the second line and
     keep `hidden` — no sideways scroll there either, just no sticky. */
  overflow-x: hidden;
  overflow-x: clip;
}

/* Four slow pools of light drift under the content. One is cool steel
   against three warm golds: that temperature split is what reads as
   "machined" rather than "candlelit". Alphas are roughly a third of what
   they were on the dark ground — a tint on white carries much further than
   a glow on black, and at the old values these read as stains. Inset is negative so the pools can drift
   past the viewport edge without ever showing a seam.

   Animated with transform only, so this composites on the GPU and never
   repaints the page underneath it. */
body::before {
  content: "";
  position: fixed; inset: -40%;
  background:
    /* Warm key light, centred. Symmetry is what keeps this reading as
       engineered rather than as an accidental stain in one corner. */
    radial-gradient(46% 30% at 50% 14%, rgba(138,109,18,.16), transparent 68%),
    /* Cool fill low and wide, well away from the warm key — kept apart so
       the two never average into grey where they overlap. */
    radial-gradient(70% 32% at 50% 96%, rgba(74,132,176,.15), transparent 72%),
    radial-gradient(30% 26% at 12% 60%, rgba(198,150,70,.11), transparent 72%),
    radial-gradient(30% 26% at 88% 62%, rgba(198,150,70,.11), transparent 72%);
  animation: aurora 48s ease-in-out infinite alternate;
  will-change: transform;
  pointer-events: none; z-index: 0;
}

/* A faint machined grid over the light, masked so it's only present up top
   and dissolves before it reaches the cards. Reads as a surface with a
   measure to it rather than blank stock — and staying under ~6% alpha is
   what keeps it texture instead of graph paper. */
body::after {
  content: "";
  position: fixed; inset: 0;
  background-image:
    /* Two grids at a 4:1 ratio — a fine measure inside a coarse one, the way
       a rule is subdivided. One grid alone just reads as a table. */
    linear-gradient(rgba(138,109,18,.10) 1px, transparent 1px),
    linear-gradient(90deg, rgba(138,109,18,.10) 1px, transparent 1px),
    linear-gradient(rgba(138,109,18,.045) 1px, transparent 1px),
    linear-gradient(90deg, rgba(138,109,18,.045) 1px, transparent 1px);
  background-size: 128px 128px, 128px 128px, 32px 32px, 32px 32px;
  /* Anchored to the top centre and gone by two-thirds down, so it frames the
     header and never runs underneath the card gallery. */
  -webkit-mask-image: radial-gradient(105% 68% at 50% 0%, #000 0%, rgba(0,0,0,.55) 38%, transparent 76%);
          mask-image: radial-gradient(105% 68% at 50% 0%, #000 0%, rgba(0,0,0,.55) 38%, transparent 76%);
  pointer-events: none; z-index: 0;
}

/* A floor. The grid above (body::after) gives the page a measured ceiling
   and then nothing answers it — light pools in the middle and the bottom of
   the screen is flat black, which reads as "dark page" rather than as space.

   This is the same grid put into perspective and laid down: real rotateX on
   a real perspective, so the lines converge on a true vanishing point and
   bunch toward the horizon the way a floor actually does. Faking the
   convergence with a squeezed background-size gets the spacing wrong at
   exactly the place the eye checks it.

   Gold at 8%, not neon — this app is obsidian and gold, and a cyan
   Tron floor would be a different product's idea of the future. Masked to
   nothing well before the horizon, so it dissolves into the dark instead of
   terminating on a hard line.

   Lives on html, not body, because body's two pseudo-elements are already
   spent on the light pools and the ceiling grid. It paints behind both:
   body's own background propagates to the canvas, and html::before is the
   first box painted over that canvas. */
html::before {
  content: "";
  position: fixed;
  /* Overhung sideways because rotateX on a perspective widens the near edge
     past the viewport — without the overhang the floor has visible side
     seams where the lines just stop. */
  left: -55%; right: -55%; bottom: 0; height: 82vh;
  background-image:
    repeating-linear-gradient(90deg, rgba(42,35,24,.07) 0 1px, transparent 1px 68px),
    repeating-linear-gradient(0deg,  rgba(42,35,24,.07) 0 1px, transparent 1px 68px);
  transform: perspective(620px) rotateX(74deg);
  transform-origin: 50% 100%;
  /* Fades out going UP, i.e. toward the horizon — the far distance goes dark
     first, which is what sells it as depth rather than as a tilted texture. */
  -webkit-mask-image: linear-gradient(to top, #000 0%, rgba(0,0,0,.62) 46%, transparent 94%);
          mask-image: linear-gradient(to top, #000 0%, rgba(0,0,0,.62) 46%, transparent 94%);
  pointer-events: none; z-index: 0;
}

/* The horizon itself: one soft bar of light where the floor vanishes, plus
   the cool wash above it. Separate from the pools in body::before because
   those drift and rotate — a horizon that wanders isn't a horizon. */
html::after {
  content: "";
  position: fixed; inset: 0;
  background:
    radial-gradient(40% 7% at 50% 78%, rgba(138,109,18,.05), transparent 72%),
    radial-gradient(85% 18% at 50% 88%, rgba(74,132,176,.04), transparent 78%);
  pointer-events: none; z-index: 0;
}

/* Established convention in this codebase: motion is decorative, never
   load-bearing. The pools hold their opening position instead of drifting. */
@media (prefers-reduced-motion: reduce) {
  body::before { animation: none; }
}

@keyframes aurora {
  0%   { transform: translate3d(-3%, -2%, 0) rotate(0deg)  scale(1);    }
  50%  { transform: translate3d( 3%,  2%, 0) rotate(5deg)  scale(1.09); }
  100% { transform: translate3d(-2%,  3%, 0) rotate(-4deg) scale(1.04); }
}

#root { position: relative; z-index: 1; }

a { color: inherit; }
input, textarea, button { font-family: inherit; }
/* A placeholder is text under WCAG 1.4.3, not a decorative hint — several of
   these fields carry no visible <label> at all, so it is the only thing
   naming them.

   This was `rgba(242,237,224,.50)` — cream at half strength, correct against
   the obsidian well it was written for. UI.field is #FFFFFF now, where the
   same cream lands at about 1.05:1 and the word simply is not on the screen:
   the design gallery's "Search designs…" read as an empty box with a caret
   blinking in it. #7A6E5A is VAULT.dim (api.js), already the placeholder for
   the Gift Vault's white-on-cream fields; on white it is 5.0:1, readable but
   still plainly quieter than typed input at 15:1.

   That makes the whole app the case .on-light was added for, so the two rules
   now agree. The class is left in place rather than stripped from the fields
   that carry it — it is what those fields say they need, and it costs a
   selector. */
input::placeholder, textarea::placeholder { color: #7A6E5A; }
.on-light::placeholder { color: #7A6E5A; }

/* #71570D (UI.gold), not the dark build's #E8C766, which is 1.6:1 on white —
   a keyboard focus ring nobody can see is the same as no focus ring. The
   outline sits outside the element via the offset, so it lands on the page
   ground rather than on a gold button's own fill. */
button:focus-visible, input:focus-visible, textarea:focus-visible, a:focus-visible {
  outline: 2px solid #71570D;
  outline-offset: 2px;
}

@keyframes sheen { 0%,100%{transform:translateX(-120%)} 50%{transform:translateX(120%)} }
@keyframes rise { from{transform:translateY(160px) scale(.9);opacity:0} to{transform:translateY(0) scale(1);opacity:1} }
@keyframes popIn { from{transform:scale(.85);opacity:0} to{transform:scale(1);opacity:1} }
@keyframes slowSpin { to{transform:rotate(360deg)} }
@keyframes flapUp { to{transform:rotateX(178deg)} }
@keyframes sealLeft { to{transform:translate(-46px,26px) rotate(-38deg);opacity:0} }
@keyframes sealRight { to{transform:translate(46px,30px) rotate(34deg);opacity:0} }
@keyframes burst { from{transform:translate(0,0) rotate(0)} to{transform:translate(var(--tx),var(--ty)) rotate(var(--rot));opacity:0} }
@keyframes float { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-8px)} }
@keyframes curtainOpenLeft { from{transform:translateX(0)} to{transform:translateX(-101%)} }
@keyframes curtainOpenRight { from{transform:translateX(0)} to{transform:translateX(101%)} }
@keyframes orbitSpin { to{transform:rotate(360deg)} }
@keyframes orbitSpinRev { to{transform:rotate(-360deg)} }

/* Showpiece layouts (the paid tier). Each is a slow ambient loop, not a
   transition — every one of these degrades to a still frame under the
   reduced-motion rule below, and each layout is composed so that still
   frame is the one worth looking at. */
@keyframes foilShift { 0%,100%{transform:translate(-12%,-12%) rotate(0deg)} 50%{transform:translate(12%,12%) rotate(180deg)} }
@keyframes auroraDrift { 0%,100%{transform:translate(-14%,0) scaleY(1)} 50%{transform:translate(14%,-6%) scaleY(1.25)} }
@keyframes twinkle { 0%,100%{opacity:.25} 50%{opacity:.9} }
@keyframes beamPulse { 0%,100%{opacity:.55} 50%{opacity:.95} }

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

/* Text for assistive technology only — present in the accessibility tree,
   painted nowhere. Clipped rather than `display:none` or `visibility:hidden`,
   both of which remove it from that tree as well and so announce nothing.
   Used by the invitation's flip announcement, which describes a change a
   sighted guest sees happen and a screen-reader user otherwise cannot. */
.sr-only {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%);
  white-space: nowrap; border: 0;
}

/* The header's account line — who you're signed in as, on what plan — sits
   in the top-right corner rather than taking a centred row of its own under
   the wordmark. It's reference information, looked at deliberately or not at
   all, so it doesn't need to be in the path of someone reading downward.

   Only once there's room beside the centred wordmark. Below that the corner
   would either overlap the mark or squeeze it, so it falls back to the
   stacked row it always was — the same reason .sticky-preview has a floor.
   AccountBar itself sets no margin or alignment; placement is decided here,
   so the two arrangements can't fight each other. */
.header-account, .header-account-static { margin-top: 14px; text-align: center; }

/* This used to pin to the top-RIGHT corner above 760px, on the reasoning that
   it is reference information and does not belong in the path of someone
   reading downward. Two things were wrong with that in practice: the line has
   grown (address, plan, a My-invitations pill, sign out) and ran off the edge
   at ordinary window widths, and "My invitations" is not reference at all —
   it is how a returning host gets back into an event, one of the most-used
   controls on the site.

   So it stays where the narrow layout always put it: a centred row directly
   under the wordmark, at every width. Nothing to clip against, and the pill
   sits where the eye already is. */

/* "Make it yours" step: the card preview stays in view while the settings
   beside it scroll, so a host can see what each change does without
   scrolling back up. Only once the two columns actually sit side by side —
   stacked, a sticky card would pin itself above the form and eat the scroll
   space instead of helping.

   650px is that point, and it is arithmetic rather than a guess: the row is
   the rendered card (272px at scale 0.85) + its 36px gap + the settings
   column's `flex: 1 1 300px` basis = 608px of content, inside S.wrap's 20px
   padding on each side. Below ~648px viewport the row wraps.

   This said 720px and was measured wrong, which left a dead band from ~648
   to 720 where the customizer showed two columns and the card scrolled away
   anyway — the width a laptop's split window or a preview pane most often
   lands on, so it read as the feature simply not working. If the card scale,
   the gap or that flex-basis changes, this number moves with them. */
/* 500px, and the arithmetic has to keep agreeing with host.jsx or this rule
   actively breaks the screen. Sticky only behaves when the two columns are
   genuinely on one line; if the row has wrapped, sticky does not stack the
   preview above the form, it pins the preview and lets the form scroll
   underneath it — the card ends up floating over the controls.

   That is what happened between about 500 and 570px: the envelope preview
   rendered 272px wide against a 240px settings basis, which does not fit,
   while this rule went on claiming it did. Both previews are now ~198px and
   the basis is 230, so the line needs 198 + 18 + 230 = 446px inside the 460
   a 500px viewport leaves after S.wrap's 20px padding. Below 500 the row
   wraps and the preview is static, which stacks properly. */
/* The `position: sticky` that used to live here now sits in host.jsx, applied
   inline beside the very sizes that decide whether the row is two columns.
   They were separate, and drifted: the preview grew, the row began wrapping,
   and this rule went on pinning a card in a stacked layout. Only the
   compositing fix below is still CSS's job. */

/* The card inside this column is a 3D layer — TiltCard sets `perspective`
   and rotates the face on pointer move. A 3D-transformed child inside a
   `position: sticky` parent is a known Chromium repaint hole: as the sticky
   element repositions, the child's layer is not invalidated and the old
   paint stays on the screen, so the card, its caption and the Change-design
   button all appear twice, offset, with the stale copy behind.

   Promoting the column to its own compositing layer makes it repaint as a
   unit and the ghost goes away. `will-change` rather than a translateZ hack
   so the intent is legible, and backface-visibility for the same reason on
   the 3D child itself. One element, so the memory cost is a rounding error. */
.sticky-preview {
  will-change: transform;
  backface-visibility: hidden;
}

/* "Choose a card": the category filter is 19 chips. Wrapped, that's five
   rows of chrome stacked above the first actual design — the gallery is
   what this screen is for, and it started below the fold. One horizontal
   strip instead, the same pattern any store's category rail uses.

   The mask fades both ends so there's a visible cue that the row continues
   past the edge (the scrollbar itself is hidden — a permanent bar under a
   single row of pills is noisier than the pills). Children are real
   buttons, so tabbing through them scrolls the strip natively; nothing
   here is reachable only by dragging. */

/* Category filter rows. Twenty-two outlined boxes stacked in a column is
   both busy and inert — the borders draw more attention than the words, and
   nothing responds to the pointer. Borderless rows with a hover and a gold
   rail on the active one read as a list you are moving through rather than a
   grid of buttons. The rail is a pseudo-element so it can animate in from
   nothing without shifting the text. */
.cat-opt {
  position: relative;
  transition: background-color .16s ease, color .16s ease, padding-left .16s ease;
}
.cat-opt::before {
  content: "";
  position: absolute; left: 0; top: 6px; bottom: 6px; width: 2px;
  /* UI.gold. #E8C766 is the dark build's gold — a glow, 1.6:1 on this
     panel, so the mark showing which category is selected was invisible
     on the light chrome. */
  background: #71570D;
  transform: scaleY(0);
  transform-origin: 50% 50%;
  transition: transform .18s cubic-bezier(.2,.8,.2,1);
}
.cat-opt:hover { background: rgba(42,35,24,.07); padding-left: 16px; }
.cat-opt:hover::before { transform: scaleY(.55); }
.cat-opt[aria-current="true"] { padding-left: 16px; }
.cat-opt[aria-current="true"]::before { transform: scaleY(1); }
.cat-opt:focus-visible { outline: 1px solid #71570D; outline-offset: -2px; }

@media (prefers-reduced-motion: reduce) {
  .cat-opt, .cat-opt::before { transition: none; }
}

/* Design tiles — the gallery grid and the "Used before" row.

   These opt out of the app-wide focus ring. Around a tile it drew a hard
   rectangle enclosing the card AND its caption, which reads as a frame
   printed on the design rather than as a focus state — on a ticket layout
   that already has its own border, it read as part of the artwork.

   The card still has to say where the keyboard is: the gallery is thousands
   of cards deep and unusable if focus is silent. So focus gets the same
   gesture hover already had — the card lifts off the page and casts a
   shadow — which is a change of state rather than a line drawn around one.
   Both are on .design-tile-face, so the caption stays put and only the card
   moves. */
.design-tile:focus-visible { outline: none; }
.design-tile-face { transition: transform .25s ease, filter .25s ease; }
.design-tile:hover .design-tile-face {
  transform: translateY(-6px) rotate(-1deg);
  filter: drop-shadow(0 10px 16px rgba(42,35,24,.16));
}
/* Focus lifts a touch further than hover: a pointer user is already looking
   at what they are pointing at, a keyboard user is looking for it. */
.design-tile:focus-visible .design-tile-face {
  transform: translateY(-9px) rotate(-1deg);
  filter: drop-shadow(0 12px 20px rgba(42,35,24,.30));
}

@media (prefers-reduced-motion: reduce) {
  /* The global rule kills the transition, not the transform — the tile still
     lifts, it just arrives instantly. That is the point: the indicator has to
     survive, only the travel is unwelcome. */
  .design-tile-face { transition: none; }
}

/* ---- Precision layer ----------------------------------------------------
   Small global rules that make the chrome read as instrumentation rather than
   as a document. Each one is here, not inline, because it applies to every
   control at once and inline styles cannot express :hover or :focus-visible. */

/* Figures line up in a column. Anywhere a number changes while someone
   watches — a gift total, a guest count, a price — proportional digits make
   the whole row jump sideways as 9 becomes 10. */
body { font-variant-numeric: tabular-nums; }

/* Cards and their contents are stationery and opt back out: a proportional
   face is correct on an invitation, and lining figures there look like a
   receipt. */
.cardface, .cardface * { font-variant-numeric: normal; }

/* A square bracket, not a glow. Two offset outlines read as a machined
   registration mark rather than a browser default, and the offset keeps the
   inner one clear of the control's own border. */
:focus-visible {
  outline: 1px solid #71570D;
  outline-offset: 2px;
}

/* Buttons answer the pointer. 1px of travel is enough to feel mechanical
   without becoming a bounce; the active state pushes it back down so a
   press reads as a press. */
button, a[role="button"] { -webkit-tap-highlight-color: transparent; }
button:not(:disabled):hover, a[role="button"]:hover { transform: translateY(-1px); }
button:not(:disabled):active, a[role="button"]:active { transform: translateY(0); }

/* Inputs light their own edge on focus rather than relying solely on the
   outline, so the field being edited is obvious at a glance across a long
   form. */
input:focus, textarea:focus, select:focus {
  border-color: #71570D !important;
  box-shadow: 0 0 0 3px rgba(113,87,13,.10);
}

@media (prefers-reduced-motion: reduce) {
  button:hover, a[role="button"]:hover,
  button:active, a[role="button"]:active { transform: none; }
}
