/**
 * dialog-panel basic centered modal styles
 *
 * This provides a simple fade in/out animation for a centered modal dialog.
 * Developers can customize or replace these styles to match their design.
 */

/* Wrapper doesn't affect layout */
dialog-panel {
	display: contents;
}

/* Base dialog styles with transitions */
dialog-panel > dialog {
	border: none;
	padding: 0;
	max-width: 32rem;
	width: 90vw;
	max-height: 85vh;
	overflow: visible;
	box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);

	/* Start hidden */
	opacity: 0;
	transform: scale(0.95);

	/* Transition always defined on base */
	transition:
		opacity 0.3s ease-out,
		transform 0.3s ease-out;
}

/* <dialog-backdrop> is THE overlay surface across the component family —
   dialog-panel, sheet, bottom-sheet and anything downstream all paint
   here, so one element and one set of tokens describe the overlay
   everywhere. connectedCallback creates it unconditionally, so it is
   always available and no consumer has to opt in.

   It is also what makes a morph transport work: a proxy engine flies its
   blob in normal flow (@magic-spells/morph-engine appends it to
   document.body), so the overlay has to be a normal DOM element the blob
   can pass ABOVE. A top-layer ::backdrop would paint over the blob and
   its backdrop-filter would blur the very thing in flight.

   The native ::backdrop is therefore kept transparent — still present,
   still clickable, so outside-click detection via the bounding-rect
   check is unaffected. */
dialog-panel > dialog::backdrop {
	background: transparent;
}

/* The z-index is deliberately modest and tokenized. An extreme value
   (this used to be 9999999) puts a backdrop-filter-capable layer at the
   very top of the stacking order, and every showModal() top-layer
   insertion then forces the compositor to re-sort and re-rasterize
   around it — a whole-page GPU job landing on the open frame, visible
   as an intermittent, load-dependent flicker on heavy pages. A sane
   value keeps the layer order stable across promotion; raise the token
   only as far as the page's own stacking actually requires.

   Layer stability is the other half of that fix, and it is handled by
   `display` and `will-change` below.

   `display: none` while closed is what keeps the overlay as cheap as a
   ::backdrop. What made the pseudo-element fast was never that it is a
   pseudo-element — it is that it exists ONLY while the dialog is open.
   This element is persistent and 200vw x 200dvh, and the whole point of
   the single-surface contract is that consumers paint their scrim here,
   `backdrop-filter` included. A backdrop-filter forces a backdrop root:
   the compositor must isolate and snapshot everything painted behind the
   element. `opacity: 0` does NOT exempt it — the element still forms a
   stacking context and still composites — so without this the mere
   presence of a dialog-panel costs a full-viewport blur layer on first
   paint, before anything has been opened, once per panel on the page.

   It also restores atomic birth/death. A live glass layer sitting in
   normal flow when showModal() inserts the dialog into the top layer
   forces the compositor to re-sort and re-rasterize the backdrop root
   around that insertion — the open-frame flicker again. Rendering the
   element only from `showing` onward means it materializes in the same
   style pass as showModal(), so there is one build instead of a re-sort.

   The opacity transition is unaffected: `showing` renders the element at
   opacity 0 and two frames pass before the flip to `shown`. This is the
   same display:none -> transition dance the <dialog> itself already
   performs at showModal(). It also gives the element a first-render
   moment, so @starting-style works on it if a consumer wants one.

   `will-change: opacity` belongs on the base rule, not scoped to the
   animating states. Scoping it there would add the hint on the open
   frame — #setState('showing') and showModal() run in the SAME task —
   forcing a full-viewport layer promotion at exactly the moment being
   kept quiet. On the base rule it is free while `display: none` (a
   non-rendered element holds no compositor layer) and already present
   on the first rendered frame. */
dialog-backdrop {
	display: none;
	position: fixed;
	background: rgba(0, 0, 0, 0.3);
	opacity: 0;
	transition: opacity 0.3s ease-out;
	pointer-events: none;
	will-change: opacity;
	z-index: var(--dialog-backdrop-z-index, 1000);
	top: 50%;
	left: 50%;
	width: 200vw;
	height: 200dvh;
	transform: translateY(-50%) translateX(-50%);
}

/* Rendered only while the panel is open or animating. Keyed off the
   panel's own state, so a hand-authored <dialog-backdrop> stays
   non-rendering between parse and custom-element upgrade too. */
dialog-panel[state='showing'] > dialog-backdrop,
dialog-panel[state='shown'] > dialog-backdrop,
dialog-panel[state='hiding'] > dialog-backdrop {
	display: block;
}

/* Showing state - dialog is open but still at initial values
   (this is the starting point for the fade-in animation) */
dialog-panel[state='showing'] > dialog {
	opacity: 0;
	transform: scale(0.95);
}

/* Shown state - fully visible (animates from showing → shown) */
dialog-panel[state='shown'] > dialog {
	opacity: 1;
	transform: scale(1);
}

/* Hiding state - faster exit animation */
dialog-panel[state='hiding'] > dialog {
	opacity: 0;
	transform: scale(0.95);
	transition:
		opacity 0.2s ease-in,
		transform 0.2s ease-in;
}

/* dialog-backdrop state animations */
dialog-panel[state='showing'] > dialog-backdrop,
dialog-panel[state='hidden'] > dialog-backdrop {
	opacity: 0;
	pointer-events: none;
}

dialog-panel[state='shown'] > dialog-backdrop {
	opacity: 1;
	pointer-events: auto;
}

dialog-panel[state='hiding'] > dialog-backdrop {
	opacity: 0;
	pointer-events: none;
	transition: opacity 0.2s ease-in;
}

/* ============================================================
   Position variants — drawer-style slide-in animations.
   Self-contained: declares its own `position: fixed` and
   explicit inset values so it doesn't rely on the UA's
   `dialog:modal` styling or on consumer body overflow rules.
   Default (no position attr) uses the centered fade/scale
   animation defined above.
   ============================================================ */

/* Shared base for all four drawer positions */
dialog-panel[position='top'] > dialog,
dialog-panel[position='right'] > dialog,
dialog-panel[position='bottom'] > dialog,
dialog-panel[position='left'] > dialog {
	position: fixed;
	margin: 0;
	max-width: 100%;
	opacity: 1;
}

/* ---- Top drawer ---- */
dialog-panel[position='top'] > dialog {
	top: 0;
	right: 0;
	bottom: auto;
	left: 0;
	width: 100%;
	max-height: 85vh;
	transform: translateY(-100%);
}

dialog-panel[position='top'][state='shown'] > dialog {
	transform: translateY(0);
}

/* ---- Bottom drawer ---- */
dialog-panel[position='bottom'] > dialog {
	top: auto;
	right: 0;
	bottom: 0;
	left: 0;
	width: 100%;
	max-height: 85vh;
	transform: translateY(100%);
}

dialog-panel[position='bottom'][state='shown'] > dialog {
	transform: translateY(0);
}

/* ---- Left drawer ---- */
dialog-panel[position='left'] > dialog {
	top: 0;
	right: auto;
	bottom: 0;
	left: 0;
	width: min(24rem, 90vw);
	height: 100dvh;
	max-height: 100dvh;
	transform: translateX(-100%);
}

dialog-panel[position='left'][state='shown'] > dialog {
	transform: translateX(0);
}

/* ---- Right drawer ---- */
dialog-panel[position='right'] > dialog {
	top: 0;
	right: 0;
	bottom: 0;
	left: auto;
	width: min(24rem, 90vw);
	height: 100dvh;
	max-height: 100dvh;
	transform: translateX(100%);
}

dialog-panel[position='right'][state='shown'] > dialog {
	transform: translateX(0);
}

/* ============================================================
   Page scroll lock — prevents the underlying page from
   scrolling while any dialog-panel is open or animating.
   Keyed on the package's own [state='*'] attributes so it
   activates from the first frame of the opening animation
   and stays through the closing animation, regardless of
   when the native <dialog>'s [open] attribute is toggled.
   ============================================================ */
body:has(dialog-panel[state='showing']),
body:has(dialog-panel[state='shown']),
body:has(dialog-panel[state='hiding']) {
	overflow: hidden;
}

/* Morph transport — the engine owns opacity, transforms, and transitions */
dialog-panel[morph] > dialog,
dialog-panel[morph][state] > dialog {
	position: fixed;
	inset: 0;
	margin: auto;
	max-width: none;
	max-height: none;
	opacity: 1;
	transform: none;
	transition: none;
}

/* Bring in the scrim during the outbound morph flight. Unlike the CSS
   path — which holds opacity 0 through `showing` and fades on the flip
   to `shown` — the morph path paints the scrim at full opacity in
   `showing` itself, because promotion to `shown` waits for the spring
   to settle. That makes `showing` both the first rendered frame and
   the opacity-1 frame, so a transition alone has nothing to start
   from; @starting-style supplies the missing first-frame value. Where
   unsupported the scrim appears instantly, which was the old behavior. */
dialog-panel[morph][state='showing'] > dialog-backdrop {
	opacity: 1;
	pointer-events: auto;
}

@starting-style {
	dialog-panel[morph][state='showing'] > dialog-backdrop {
		opacity: 0;
	}
}
