// All selectors MUST be scoped under .scrying-pool. // Use --sp-* tokens only — no Foundry --color-* / --font-* / --border-* tokens allowed. // Implemented in story 1.5. // ============================================================ // CSS Custom Properties (State Tokens) // ============================================================ :root { --sp-state-active: hsl(140, 60%, 55%); --sp-state-hidden: hsl(0, 0%, 50%); --sp-state-self-muted: hsl(200, 60%, 55%); --sp-state-cam-lost: hsl(30, 80%, 55%); --sp-state-pending: hsl(50, 90%, 55%); --sp-urgency-director: hsl(38, 90%, 55%); --sp-state-color: hsl(140, 60%, 55%); // default, overridden per state } // ============================================================ // ScryingPoolStrip Layout // ============================================================ // Outer Foundry Application window (has .scrying-pool-strip via defaultOptions.classes). // Only visual appearance — sizing is controlled by JS setPosition(). // WARNING: do NOT add max-width or overflow here; the outer window also carries this class // and would clip the expanded inner content. .scrying-pool-strip { background: rgba(0, 0, 0, 0.01); border-radius: 8px; transition: background 0.25s ease; &:hover { background: rgba(0, 0, 0, 0.3); } /* Override FoundryVTT v14's .application min-width: 200px — strip sizing is controlled by JS */ min-width: unset; /* Ensure strip appears below Director's Board (z-index: 100) */ z-index: 50; // Hide Foundry's default window header — replaced by a lightweight in-content button. header.window-header { display: none; } // Remove window-content padding so the strip fills the frame edge-to-edge. .window-content { padding: 0; overflow: hidden; } } // Inner template div (has BOTH .scrying-pool AND .scrying-pool-strip). // Controls the expand/collapse behaviour; safe to use max-width + overflow here. .scrying-pool.scrying-pool-strip { position: relative; display: flex; flex-direction: column; align-items: center; max-width: calc(var(--sp-widget-width, 83px) + 11px); overflow: hidden; transition: max-width 200ms ease-in-out; // Horizontal and mosaic layouts: width is controlled by JS setPosition &.sp-layout-horizontal-sm, &.sp-layout-horizontal-md, &.sp-layout-mosaic-sm, &.sp-layout-mosaic-md { max-width: none; align-items: flex-start; } } // ── Horizontal layout ──────────────────────────────────────────────────────── .scrying-pool.scrying-pool-strip.sp-layout-horizontal-sm, .scrying-pool.scrying-pool-strip.sp-layout-horizontal-md { .sp-strip__participants { flex-direction: row; flex-wrap: wrap; padding: 4px; gap: 4px; justify-content: flex-start; width: auto; } } // ── Mosaic layout ──────────────────────────────────────────────────────────── .scrying-pool.scrying-pool-strip.sp-layout-mosaic-sm, .scrying-pool.scrying-pool-strip.sp-layout-mosaic-md { .sp-strip__participants { display: grid; padding: 4px; gap: 4px; // Keep width: 100% (from base) so auto-fill has a definite inline size } } .scrying-pool.scrying-pool-strip.sp-layout-mosaic-sm .sp-strip__participants { grid-template-columns: repeat(auto-fill, var(--sp-widget-width, 83px)); } .scrying-pool.scrying-pool-strip.sp-layout-mosaic-md .sp-strip__participants { grid-template-columns: repeat(auto-fill, var(--sp-widget-width, 150px)); } // ── Medium tile size for horizontal / mosaic ───────────────────────────────── .scrying-pool.scrying-pool-strip.sp-layout-horizontal-md, .scrying-pool.scrying-pool-strip.sp-layout-mosaic-md { .sp-participant-avatar { width: var(--sp-widget-width, 150px); height: var(--sp-widget-width, 150px); flex-direction: column; align-items: center; padding: 8px 4px 4px; gap: 4px; .sp-avatar__shell { width: calc(var(--sp-widget-width, 150px) - 59px); height: calc(var(--sp-widget-width, 150px) - 59px); } .sp-avatar__name { display: block; font-size: 0.65rem; text-align: center; width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .sp-avatar__corner-badge { bottom: 4px; right: 4px; } // Remove the is-expanded overlay styles that interfere with tile layout &::after { display: none; } } } // ── Toolbar: grip + toggle + DB + close on one line ───────────────────────── // All chrome lives here; hidden at rest, revealed on strip hover. .sp-strip__toolbar { display: flex; flex-direction: row; align-items: center; width: 100%; height: 24px; flex-shrink: 0; opacity: 0; pointer-events: none; transition: opacity 0.2s; .scrying-pool-strip:hover & { opacity: 1; pointer-events: auto; } } // Drag grip — compact icon handle .sp-strip__grip { width: 20px; height: 24px; display: flex; align-items: center; justify-content: center; cursor: grab; color: var(--sp-text, hsl(0, 0%, 80%)); font-size: 10px; flex-shrink: 0; user-select: none; &:active { cursor: grabbing; } .scrying-pool-strip:hover & { opacity: 0.45; &:hover { opacity: 0.75; } &:active { opacity: 1; } } } // Toggle (expand/collapse) .sp-strip__toggle { height: 24px; min-width: 22px; width: 22px; display: flex; align-items: center; justify-content: center; background: none; border: none; cursor: pointer; color: var(--sp-text, hsl(0, 0%, 80%)); font-size: 10px; flex-shrink: 0; opacity: 0.6; &:hover { opacity: 1; } } // Director's Board CTA button .sp-strip__directors-board-cta { width: 22px; height: 24px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; background: none; border: none; cursor: pointer; color: var(--sp-text-secondary, #7a8390); font-size: 11px; transition: background 0.15s, color 0.15s; &:hover { background: rgba(255, 255, 255, 0.06); color: var(--sp-text-primary, #dde2e8); } } // Spacer — pushes close button to the right .sp-strip__toolbar-spacer { flex: 1; } // Close button — in-toolbar, no longer absolutely positioned .sp-strip__close-btn { width: 20px; height: 24px; padding: 0; line-height: 24px; text-align: center; font-size: 13px; font-weight: 400; background: transparent; color: var(--sp-text, hsl(0, 0%, 80%)); border: none; cursor: pointer; flex-shrink: 0; opacity: 0.45; transition: opacity 0.2s, background 0.15s; &:hover { opacity: 1; background: rgba(255, 255, 255, 0.1); } &:active { opacity: 0.75; } } .sp-strip__participants { list-style: none; margin: 0; padding: 0; width: 100%; display: flex; flex-direction: column; gap: 4px; } .sp-strip__participant-item { margin: 0; // override browser/Foundry default
  • margins } .sp-strip__first-tip { font-size: 0.75rem; color: var(--sp-text-muted, hsl(0, 0%, 60%)); padding: 4px 8px; margin: 0; opacity: 0; transition: opacity 0.2s; .scrying-pool-strip:hover & { opacity: 1; } } // ============================================================ // ParticipantAvatar (44×44px container, 32px rounded image) // ============================================================ .sp-participant-avatar { position: relative; width: 83px; height: var(--sp-widget-width, 83px); display: flex; align-items: center; justify-content: flex-start; background: none; border: none; cursor: pointer; padding: 6px; border-radius: 4px; gap: 8px; overflow: hidden; &:focus-visible { outline: 2px solid var(--sp-focus-ring, hsl(200, 80%, 60%)); outline-offset: 2px; } .is-expanded & { display: flex; align-items: center; justify-content: center; width: 100%; height: auto; padding: 5px; background: transparent; &:hover { background: hsl(220, 15%, 14%); } &::after { display: none; } // Shell: sized to widget width, centered in the card .sp-avatar__shell { width: calc(var(--sp-widget-width) - 59px); height: calc(var(--sp-widget-width) - 59px); } // No names displayed in vertical mode .sp-avatar__name, .sp-avatar__state-label { display: none; } .sp-avatar__corner-badge { bottom: 6px; right: 6px; z-index: 4; width: 10px; height: 10px; } } } // Border shell — no clip-path, no background. Serves only as the positioning // parent for ::before (border), ::after (state ring), and .sp-avatar__shape (content). // Border and ring use the SAME clip-path as the shape but are scaled up via // negative inset, creating a genuine border/ring around the clipped shape. .sp-avatar__shell { position: relative; flex-shrink: 0; display: flex; align-items: center; justify-content: center; // Default sm size — overridden by layout-specific CSS width: 60px; height: 60px; } // Tile border — ::before with same clip-path as shape, scaled up by border-width. // Hidden via opacity when --sp-tile-border-active is 0 (border-width = 0) // so the border background doesn't bleed through transparent avatar areas. .sp-avatar__shell::before { content: ''; position: absolute; inset: calc(var(--sp-tile-border-width, 0px) * -1); background: var(--sp-tile-border-color, transparent); clip-path: var(--sp-shape-clip, none); opacity: var(--sp-tile-border-active, 1); z-index: -3; pointer-events: none; } // State ring — ::after with same clip-path, scaled up same as border. // Uses inset box-shadow so the ring appears INSIDE the clipped area, // overlaying either the border (via ::before behind) or the avatar content. // At z-index 1 it renders above .sp-avatar__shape so the ring is visible // on top of the avatar video/img. // Gated by --sp-tile-border-active alongside the border ::before so that // "None" border truly removes ALL colored rings. .sp-avatar__shell::after { content: ''; position: absolute; inset: calc(var(--sp-tile-border-width, 0px) * -1); background: transparent; box-shadow: inset 0 0 0 2px transparent; clip-path: var(--sp-shape-clip, none); opacity: var(--sp-tile-border-active, 1); z-index: 1; pointer-events: none; transition: box-shadow 200ms ease, opacity 200ms ease; } // Shape wrapper inside the shell — carries clip-path for shapes. .sp-avatar__shape { position: relative; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; overflow: hidden; clip-path: var(--sp-shape-clip, none); } // Inner content (img and video) fill the shape .sp-avatar__shape .sp-avatar__img, .sp-avatar__shape .sp-participant-video { width: 100%; height: 100%; } // Video container for WebRTC stream (full AV replacement mode) // Regular flow child inside .sp-avatar__shell — contributes to shell sizing .sp-participant-video { display: flex; align-items: center; justify-content: center; overflow: hidden; } .sp-avatar__img { width: 60px; height: 60px; object-fit: cover; flex-shrink: 0; } // Video element styling .sp-participant-video__element { width: 100%; height: 100%; object-fit: cover; background: hsl(220, 15%, 18%); .is-expanded & { border-radius: 4px; } } // Hide avatar image when video stream is active (has video element) .sp-participant-video:not(:empty) ~ .sp-avatar__img { display: none; } .sp-avatar__corner-badge { position: absolute; bottom: 2px; right: 2px; width: 12px; height: 12px; border-radius: 50%; background: var(--sp-state-color); font-size: 8px; display: flex; align-items: center; justify-content: center; } .sp-avatar__name { font-size: 0.85rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--sp-text, hsl(0, 0%, 85%)); opacity: 0; transition: opacity 0.2s; .sp-participant-avatar:hover & { opacity: 1; } } .sp-avatar__state-label { font-size: 0.7rem; color: var(--sp-text-muted, hsl(0, 0%, 60%)); opacity: 0; transition: opacity 0.2s; .sp-participant-avatar:hover & { opacity: 1; } } // ============================================================ // StateRing variants (applied as class on .sp-participant-avatar) // ============================================================ // State ring via ::after on the shell — uses inset box-shadow so the ring // follows the same clip-path as the shape, overlaying the border's inner edge. // When there's no border (width=0), the ring overlays the avatar content edge. .sp-participant-avatar.sp-state-active, .sp-participant-avatar.sp-state-self-muted { --sp-state-color: var(--sp-state-active); .sp-avatar__shell::after { box-shadow: inset 0 0 0 2px var(--sp-state-color); } } .sp-participant-avatar.sp-state-hidden, .sp-participant-avatar.sp-state-cam-lost { --sp-state-color: var(--sp-state-hidden); } .sp-participant-avatar.sp-state-pending { --sp-state-color: var(--sp-state-pending); .sp-avatar__shell::after { box-shadow: inset 0 0 0 2px var(--sp-state-color); } } // ============================================================ // StateRing animations — gated under no-preference (AC-16) // ============================================================ @media (prefers-reduced-motion: no-preference) { .sp-participant-avatar.sp-state-pending .sp-avatar__shell::after { animation: sp-pulse 2s ease-in-out infinite; } @keyframes sp-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } } // Revert flash (200ms amber, then restore) .sp-participant-avatar.sp-state-revert .sp-avatar__shell::after { animation: sp-revert-flash 200ms ease-out forwards; } @keyframes sp-revert-flash { 0% { box-shadow: inset 0 0 0 3px var(--sp-urgency-director); } 100% { box-shadow: inset 0 0 0 2px var(--sp-state-color); } } } // ============================================================ // EmptyStatePanel (AC-11) // ============================================================ .sp-strip__empty-state { display: flex; flex-direction: column; align-items: center; padding: 16px 8px; gap: 8px; color: var(--sp-text-muted, hsl(0, 0%, 60%)); } .sp-empty__icon { font-size: 1.5rem; display: block; } .sp-empty__text { font-size: 0.75rem; text-align: center; } @media (prefers-reduced-motion: no-preference) { .sp-empty__icon { animation: sp-breathe 3s ease-in-out infinite; } @keyframes sp-breathe { 0%, 100% { opacity: 0.6; transform: scale(1); } 50% { opacity: 1.0; transform: scale(1.05); } } } // ============================================================ // AV Tile overlays (applied to .camera-view[data-user-id="..."]) // ============================================================ .sp-lock-overlay { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; background: hsla(0, 0%, 0%, 0.45); pointer-events: none; z-index: 10; &::before { content: '\f023'; // fa-lock font-family: 'Font Awesome 6 Free'; font-weight: 900; font-size: 1.2rem; color: hsl(0, 0%, 85%); } } .camera-view.sp-state-hidden { opacity: 0.55; position: relative; } .sp-portrait-fallback { position: absolute; inset: 0; background: var(--sp-bg, hsl(220, 15%, 18%)) center/cover no-repeat; pointer-events: none; } // ============================================================ // Context menu // ============================================================ .sp-context-menu { background: var(--sp-bg, hsl(220, 15%, 15%)); border: 1px solid hsl(0, 0%, 30%); border-radius: 4px; padding: 4px 0; min-width: 160px; z-index: 1000; box-shadow: 0 4px 12px hsla(0, 0%, 0%, 0.4); .sp-context-menu__item { display: flex; align-items: center; gap: 8px; width: 100%; padding: 6px 12px; background: none; border: none; cursor: pointer; color: var(--sp-text, hsl(0, 0%, 85%)); font-size: 0.875rem; text-align: left; &:hover, &:focus-visible { background: hsla(200, 60%, 55%, 0.15); } } } // ============================================================ // ActionPopover () // ============================================================ .sp-action-popover { background: var(--sp-bg, hsl(220, 15%, 15%)); border: 1px solid hsl(0, 0%, 30%); border-radius: 6px; padding: 12px; min-width: 160px; box-shadow: 0 4px 16px hsla(0, 0%, 0%, 0.5); color: var(--sp-text, hsl(0, 0%, 85%)); .sp-action-popover__cta { display: block; width: 100%; padding: 8px 16px; background: hsl(200, 60%, 40%); border: none; border-radius: 4px; cursor: pointer; color: hsl(0, 0%, 95%); font-size: 0.875rem; &:hover:not(:disabled) { background: hsl(200, 60%, 50%); } &:disabled { opacity: 0.45; cursor: not-allowed; } } } // ============================================================ // Tile shape variants (applied as .sp-shape-* on .sp-participant-avatar) // ============================================================ // Defines --sp-shape-clip which is consumed by: // - .sp-avatar__shape (content clip) // - .sp-avatar__shell::before (border) // - .sp-avatar__shell::after (state ring) // All three use the same clip-path, but ::before and ::after are scaled up // via negative inset to create the border/ring around the clipped shape. .sp-participant-avatar { &.sp-shape-rounded { --sp-shape-clip: inset(0 round 6px); } &.sp-shape-circle { --sp-shape-clip: circle(50%); } &.sp-shape-hexagon { --sp-shape-clip: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%); } &.sp-shape-octagon { --sp-shape-clip: polygon(29% 0%, 71% 0%, 100% 29%, 100% 71%, 71% 100%, 29% 100%, 0% 71%, 0% 29%); } }