// 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 (