/* ── Trigger ────────────────────────────────────────────────────────────── */ [data-drawer-trigger] { display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2); min-block-size: var(--control-height-md); padding-inline: var(--space-3); border: var(--border-width) solid var(--color-border-default); border-radius: var(--radius-md); background: var(--color-surface-raised); color: var(--color-content-primary); font: inherit; font-weight: var(--font-weight-medium); cursor: pointer; } [data-drawer-trigger]:hover { background: var(--color-surface-overlay); } [data-drawer-trigger]:focus-visible { outline: var(--focus-ring-width) solid var(--focus-ring-color); outline-offset: var(--focus-ring-offset); } [data-drawer-trigger]:disabled, [data-drawer-trigger][data-disabled] { cursor: default; opacity: var(--opacity-disabled); } /* ── Overlay ────────────────────────────────────────────────────────────── */ /* Default: hidden. Only `variant='overlay'` paints the backdrop — the * `'inline'` and `'persistent'` variants don't have a backdrop concept. * The wrapper skips Overlay entirely in those modes; this rule guards * the case where a consumer renders `` explicitly * inside the compound API with a non-overlay variant. */ [data-drawer-overlay] { display: none; } [data-drawer-overlay][data-variant='overlay'] { display: block; position: fixed; inset: 0; background: color-mix( in srgb, var(--drawer-overlay-bg) var(--drawer-overlay-opacity), transparent ); backdrop-filter: blur(var(--drawer-overlay-blur)); z-index: var(--drawer-overlay-z); opacity: 1; } /* Overlay enter/exit motion comes from the motion system (preset `fade` via * `data-animation-style`). The overlay is `display:none` for inline/persistent, * so the fade only shows for `variant='overlay'`. Timing preserved via override. */ [data-drawer-overlay] { --motion-duration-enter: var(--duration-normal); --motion-duration-exit: var(--duration-normal); } /* ── Content (variant-shared base) ──────────────────────────────────────── */ [data-drawer-content] { --_drawer-size-width: var(--drawer-content-width-md); --_drawer-size-height: var(--drawer-content-height-md); --_drawer-panel-width: var(--_drawer-content-width-override, var(--_drawer-size-width)); --_drawer-panel-height: var(--_drawer-content-height-override, var(--_drawer-size-height)); --_drawer-padding: var(--drawer-content-padding-md); display: flex; flex-direction: column; gap: var(--drawer-stack-gap); padding: var(--_drawer-padding); overflow: auto; min-inline-size: var(--_drawer-content-min-width-override, auto); max-inline-size: var(--_drawer-content-max-width-override, none); min-block-size: var(--_drawer-content-min-height-override, auto); max-block-size: var(--_drawer-content-max-height-override, none); /* Elevation bundle (surface · border · shadow) from the `modal` plane via `data-depth` (A1, Decisión 8). Keeps its own radius/color; the [data-dragging] box-shadow override below still wins while dragging. */ border-radius: var(--drawer-content-radius); color: var(--drawer-content-color); outline: none; } /* Drag-state visual feedback. While the gesture is in flight (`data-dragging` * is set on Content by the runtime from the gesture state), the panel lifts * with a stronger shadow and gains a subtle outline ring. Animation is short * (no jank) and additive — does not alter layout. */ [data-drawer-content][data-dragging] { box-shadow: var(--drawer-content-shadow-dragging); outline: var(--drawer-content-ring-dragging-width) solid var(--drawer-content-ring-dragging-color); outline-offset: var(--drawer-content-ring-dragging-offset); } /* Reserved slot for exit tint on the leading edge — colored when the * morfo's prewrite has written `data-last-action` and `data-state` flips * to 'closed'. The leading edge depends on side: top→bottom, right→left, * etc. (the edge facing the user before closing). Only meaningful in * overlay/inline variants where the drawer animates out. */ [data-drawer-content][data-side='right'] { border-left: var(--border-width-thick) solid transparent; } [data-drawer-content][data-side='left'] { border-right: var(--border-width-thick) solid transparent; } [data-drawer-content][data-side='top'] { border-bottom: var(--border-width-thick) solid transparent; } [data-drawer-content][data-side='bottom'] { border-top: var(--border-width-thick) solid transparent; } /* ── Content sizing (variant-shared) ────────────────────────────────────── */ [data-drawer-content][data-size='sm'] { --_drawer-size-width: var(--drawer-content-width-sm); --_drawer-size-height: var(--drawer-content-height-sm); --_drawer-padding: var(--drawer-content-padding-sm); } [data-drawer-content][data-size='md'] { --_drawer-size-width: var(--drawer-content-width-md); --_drawer-size-height: var(--drawer-content-height-md); --_drawer-padding: var(--drawer-content-padding-md); } [data-drawer-content][data-size='lg'] { --_drawer-size-width: var(--drawer-content-width-lg); --_drawer-size-height: var(--drawer-content-height-lg); --_drawer-padding: var(--drawer-content-padding-lg); } [data-drawer-content][data-size='full'] { --_drawer-padding: var(--drawer-content-padding-full); } /* ── Variant 'overlay' — fixed positioning + backdrop stack ─────────────── */ [data-drawer-content][data-variant='overlay'] { position: fixed; z-index: calc(var(--drawer-overlay-z) + 1); } [data-drawer-content][data-variant='overlay'][data-side='right'] { top: var(--drawer-content-inset); right: var(--drawer-content-inset); bottom: var(--drawer-content-inset); width: min(calc(100vw - (var(--drawer-content-inset) * 2)), var(--_drawer-panel-width)); } [data-drawer-content][data-variant='overlay'][data-side='left'] { top: var(--drawer-content-inset); left: var(--drawer-content-inset); bottom: var(--drawer-content-inset); width: min(calc(100vw - (var(--drawer-content-inset) * 2)), var(--_drawer-panel-width)); } [data-drawer-content][data-variant='overlay'][data-side='top'] { top: var(--drawer-content-inset); left: var(--drawer-content-inset); right: var(--drawer-content-inset); height: min( calc(100dvh - (var(--drawer-content-inset) * 2)), var(--_drawer-panel-height) ); } [data-drawer-content][data-variant='overlay'][data-side='bottom'] { right: var(--drawer-content-inset); bottom: var(--drawer-content-inset); left: var(--drawer-content-inset); height: min( calc(100dvh - (var(--drawer-content-inset) * 2)), var(--_drawer-panel-height) ); } [data-drawer-content][data-variant='overlay'][data-size='full'] { inset: var(--drawer-content-inset); width: var(--_drawer-content-width-override, auto); height: var(--_drawer-content-height-override, auto); } /* ── Variant 'inline' — fixed positioning, no backdrop, lower stack ─────── */ /* Same geometry as overlay but a separate z-index band so it doesn't * fight a real modal that may also be on screen. The drawer sits over * content (background remains interactive — soma drops the focus trap * and dismissal layers in this variant). */ [data-drawer-content][data-variant='inline'] { position: fixed; z-index: var(--drawer-inline-z); } [data-drawer-content][data-variant='inline'][data-side='right'] { top: var(--drawer-content-inset); right: var(--drawer-content-inset); bottom: var(--drawer-content-inset); width: min(calc(100vw - (var(--drawer-content-inset) * 2)), var(--_drawer-panel-width)); } [data-drawer-content][data-variant='inline'][data-side='left'] { top: var(--drawer-content-inset); left: var(--drawer-content-inset); bottom: var(--drawer-content-inset); width: min(calc(100vw - (var(--drawer-content-inset) * 2)), var(--_drawer-panel-width)); } [data-drawer-content][data-variant='inline'][data-side='top'] { top: var(--drawer-content-inset); left: var(--drawer-content-inset); right: var(--drawer-content-inset); height: min( calc(100dvh - (var(--drawer-content-inset) * 2)), var(--_drawer-panel-height) ); } [data-drawer-content][data-variant='inline'][data-side='bottom'] { right: var(--drawer-content-inset); bottom: var(--drawer-content-inset); left: var(--drawer-content-inset); height: min( calc(100dvh - (var(--drawer-content-inset) * 2)), var(--_drawer-panel-height) ); } [data-drawer-content][data-variant='inline'][data-size='full'] { inset: var(--drawer-content-inset); width: var(--_drawer-content-width-override, auto); height: var(--_drawer-content-height-override, auto); } /* ── Variant 'persistent' — in flow, no insets ──────────────────────────── */ /* Persistent drawer takes its slot in a parent flex/grid layout (the * consumer's responsibility). The recipe just sizes the panel along * the drawer's main axis; the cross axis fills the parent (`100%`). */ [data-drawer-content][data-variant='persistent'] { position: relative; border-radius: 0; } [data-drawer-content][data-variant='persistent'][data-side='left'], [data-drawer-content][data-variant='persistent'][data-side='right'] { width: var(--_drawer-panel-width); block-size: 100%; } [data-drawer-content][data-variant='persistent'][data-side='top'], [data-drawer-content][data-variant='persistent'][data-side='bottom'] { inline-size: 100%; height: var(--_drawer-panel-height); } [data-drawer-content][data-variant='persistent'][data-size='full'] { width: 100%; height: 100%; } /* ── Presence motion (overlay + inline) ───────────────────────────────── * Driven by the motion system: the eidos wrapper writes * `data-animation-style='slide-full'` and the registry's side-aware preset * (full slide, keyed on `data-side`) animates enter/exit. The panel is INSET * by `--drawer-content-inset`, so it must travel its own size PLUS that gap to * fully clear the edge — `--motion-slide-leave` feeds the preset that extra. * See `eidos-motion.md` (momento --state). */ [data-drawer-content] { --motion-slide-leave: var(--drawer-content-inset); --motion-duration-enter: var(--duration-moderate); --motion-duration-exit: var(--duration-normal); } /* `persistent` is always present — it opts out of the presence animation. */ [data-drawer-content][data-variant='persistent'] { animation: none !important; } /* ── data-last-action exit tinting (overlay + inline only) ──────────────── */ [data-drawer-content][data-state='closed'][data-side='right'][data-last-action='saved'] { border-left-color: var(--color-fulfill-element); } [data-drawer-content][data-state='closed'][data-side='left'][data-last-action='saved'] { border-right-color: var(--color-fulfill-element); } [data-drawer-content][data-state='closed'][data-side='top'][data-last-action='saved'] { border-bottom-color: var(--color-fulfill-element); } [data-drawer-content][data-state='closed'][data-side='bottom'][data-last-action='saved'] { border-top-color: var(--color-fulfill-element); } [data-drawer-content][data-state='closed'][data-side='right'][data-last-action='failed'] { border-left-color: var(--color-threat-element); } [data-drawer-content][data-state='closed'][data-side='left'][data-last-action='failed'] { border-right-color: var(--color-threat-element); } [data-drawer-content][data-state='closed'][data-side='top'][data-last-action='failed'] { border-bottom-color: var(--color-threat-element); } [data-drawer-content][data-state='closed'][data-side='bottom'][data-last-action='failed'] { border-top-color: var(--color-threat-element); } [data-drawer-content][data-state='closed'][data-side='right'][data-last-action='cancelled'], [data-drawer-content][data-state='closed'][data-side='right'][data-last-action='dismissed'], [data-drawer-content][data-state='closed'][data-side='right'][data-last-action='dismissed-outside'] { border-left-color: var(--color-neutral-element); } [data-drawer-content][data-state='closed'][data-side='left'][data-last-action='cancelled'], [data-drawer-content][data-state='closed'][data-side='left'][data-last-action='dismissed'], [data-drawer-content][data-state='closed'][data-side='left'][data-last-action='dismissed-outside'] { border-right-color: var(--color-neutral-element); } [data-drawer-content][data-state='closed'][data-side='top'][data-last-action='cancelled'], [data-drawer-content][data-state='closed'][data-side='top'][data-last-action='dismissed'], [data-drawer-content][data-state='closed'][data-side='top'][data-last-action='dismissed-outside'] { border-bottom-color: var(--color-neutral-element); } [data-drawer-content][data-state='closed'][data-side='bottom'][data-last-action='cancelled'], [data-drawer-content][data-state='closed'][data-side='bottom'][data-last-action='dismissed'], [data-drawer-content][data-state='closed'][data-side='bottom'][data-last-action='dismissed-outside'] { border-top-color: var(--color-neutral-element); } /* ── Handle (canon part — air didn't ship a handle) ─────────────────────── */ /* The handle is a small grab bar at the leading edge of the drawer * (typically the only edge meant to be dragged). Orientation flips * with `data-side`: horizontal bar for top/bottom (sheet pattern), * vertical bar for left/right. Snap-point cycling handled by soma. */ [data-drawer-handle] { flex: 0 0 auto; background: var(--drawer-handle-bg); border-radius: var(--drawer-handle-radius); cursor: grab; touch-action: none; transition: background var(--drawer-handle-transition-duration) var(--drawer-handle-transition-ease), transform var(--drawer-handle-transition-duration) var(--drawer-handle-transition-ease); } [data-drawer-handle]:active { cursor: grabbing; } /* Active feedback while the gesture is live. `data-dragging` lives on * Content (set by the runtime from the Gesture state) — descendant * selector reaches into the handle. Brighter color + small bump signals * that the handle is the load-bearing element of the gesture. */ [data-drawer-content][data-dragging] [data-drawer-handle] { background: var(--drawer-handle-bg-active); cursor: grabbing; } /* Cursor persistence during drag: the Gesture layer captures the pointer, * which removes the handle from the element-under-cursor → `:active` is * lost and the cursor reverts to default. Hold `grabbing` on the entire * Content while data-dragging is on, AND on `body` via `:has()` so that * drags extending past the drawer edges still show the right cursor. */ [data-drawer-content][data-dragging] { cursor: grabbing; } body:has([data-drawer-content][data-dragging]) { cursor: grabbing; } /* Bump axis depends on which dimension is the "thickness". For top/bottom * drawers the handle is horizontal — scale Y; for left/right it's vertical * — scale X. */ [data-drawer-content][data-dragging][data-side='top'] [data-drawer-handle], [data-drawer-content][data-dragging][data-side='bottom'] [data-drawer-handle] { transform: scaleY(var(--drawer-handle-active-scale)); } [data-drawer-content][data-dragging][data-side='left'] [data-drawer-handle], [data-drawer-content][data-dragging][data-side='right'] [data-drawer-handle] { transform: translateY(-50%) scaleX(var(--drawer-handle-active-scale)); } /* For top/bottom drawers: horizontal grabber centered on the leading edge */ [data-drawer-content][data-side='top'] [data-drawer-handle], [data-drawer-content][data-side='bottom'] [data-drawer-handle] { width: var(--drawer-handle-length); height: var(--drawer-handle-thickness); margin-inline: auto; } /* For top: handle sits at the bottom edge (the "pulled" edge) * For bottom: at the top edge */ [data-drawer-content][data-side='top'] [data-drawer-handle] { order: 999; margin-block-start: auto; } [data-drawer-content][data-side='bottom'] [data-drawer-handle] { order: -1; margin-block-end: auto; } /* For left/right drawers: vertical grabber */ [data-drawer-content][data-side='left'] [data-drawer-handle], [data-drawer-content][data-side='right'] [data-drawer-handle] { width: var(--drawer-handle-thickness); height: var(--drawer-handle-length); margin-block: auto; position: absolute; top: 50%; transform: translateY(-50%); } [data-drawer-content][data-side='left'] [data-drawer-handle] { right: var(--space-2); } [data-drawer-content][data-side='right'] [data-drawer-handle] { left: var(--space-2); } /* ── Header / Footer / Title / Description ──────────────────────────────── */ [data-drawer-header] { display: flex; align-items: start; justify-content: space-between; gap: var(--drawer-header-gap); } [data-drawer-footer] { display: flex; flex-wrap: wrap; align-items: center; justify-content: end; gap: var(--drawer-footer-gap); margin-top: var(--drawer-footer-margin-top); } [data-drawer-title] { margin: 0; font-family: var(--drawer-title-font-family); font-size: var(--drawer-title-font-size); font-weight: var(--drawer-title-font-weight); line-height: var(--drawer-title-line-height); color: var(--drawer-title-color); text-wrap: balance; } [data-drawer-description] { margin: 0; font-family: var(--drawer-description-font-family); font-size: var(--drawer-description-font-size); line-height: var(--drawer-description-line-height); color: var(--drawer-description-color); } /* ── Close button ───────────────────────────────────────────────────────── */ [data-drawer-close] { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; min-inline-size: var(--drawer-close-size); block-size: var(--drawer-close-size); padding-inline: var(--drawer-close-px); border: var(--drawer-close-border-width) solid var(--drawer-close-border); border-radius: var(--drawer-close-radius); background: var(--drawer-close-bg); color: var(--drawer-close-color); cursor: pointer; transition: background var(--drawer-close-transition-duration) var(--drawer-close-transition-ease), color var(--drawer-close-transition-duration) var(--drawer-close-transition-ease), border-color var(--drawer-close-transition-duration) var(--drawer-close-transition-ease), box-shadow var(--drawer-close-transition-duration) var(--drawer-close-transition-ease); } [data-drawer-close][data-icon-only] { inline-size: var(--drawer-close-size); padding-inline: 0; } [data-drawer-close]:hover { background: var(--drawer-close-bg-hover); color: var(--drawer-close-color-hover); } [data-drawer-close]:focus-visible { outline: none; box-shadow: 0 0 0 var(--drawer-close-focus-ring-offset) var(--drawer-content-bg), 0 0 0 calc(var(--drawer-close-focus-ring-offset) + var(--drawer-close-focus-ring-width)) var(--drawer-close-focus-ring-color); } [data-drawer-close]:disabled, [data-drawer-close][data-disabled] { cursor: default; opacity: var(--drawer-close-disabled-opacity); }