# PROMPT 08 — Floating Session Toolbar Component
## Prerequisites: Prompts 01–07 complete

---

## Your Task
Implement `components/toolbar/collab-toolbar.component.ts + .html + .scss`
A globally mounted floating toolbar visible during any active session.
See `@reference/collab_prototype.html` for the visual reference.

---

## Component Setup
```typescript
@Component({
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [CdkDrag, NgClass, AsyncPipe],
  host: { class: 'collab-toolbar-host' }
})
export class CollabToolbarComponent implements OnInit, OnDestroy {
  private readonly sessionState     = inject(SessionState);
  private readonly participantState = inject(ParticipantState);
  private readonly uiState          = inject(CollabUiState);
  private readonly hubService       = inject(CollabHubService);
  private readonly sessionService   = inject(CollabSessionService);
  private readonly dialog           = inject(Dialog);
```

## Signals to expose to template
```typescript
readonly visible          = this.uiState.toolbarVisible;
readonly sessionCode      = this.sessionState.currentSessionCode;
readonly participantCount = this.sessionState.participantCount;
readonly hasControl       = this.participantState.hasControl;
readonly isConnected      = this.hubService.isConnected;
readonly isReconnecting   = this.hubService.isReconnecting;
readonly chatBadge        = this.uiState.unreadChatCount;
readonly isChatOpen       = this.uiState.chatOpen;
readonly isAnnotating     = this.uiState.annotateMode;
readonly isScreenSharing  = this.uiState.screenShareActive;
readonly elapsed          = signal<string>('00:00');  // live timer
```

## Live Timer
```typescript
// Start interval when session becomes active
// Read startedAt from sessionState.currentSession()
// Update elapsed signal: "04:51" format under 1hr, "1h 23m" over 1hr
// Use effect() to start/stop interval when session changes
// Clear interval in ngOnDestroy
```

## Screen Share Action
```typescript
async toggleScreenShare(): Promise<void> {
  if (this.isScreenSharing()) {
    // Stop: stop media tracks, uiState.setScreenShareActive(false)
    return;
  }
  try {
    const stream = await navigator.mediaDevices.getDisplayMedia({ video: true });
    // Store stream reference for later stopping
    stream.getVideoTracks()[0].onended = () => this.uiState.setScreenShareActive(false);
    this.uiState.setScreenShareActive(true);
  } catch {
    // User cancelled or denied — silent fail
  }
}
```

## End Session Action
```typescript
async endSession(): Promise<void> {
  const ref = this.dialog.open(ConfirmDialogComponent, {
    data: { message: 'End session for all participants?', confirmLabel: 'End Session', danger: true }
  });
  const confirmed = await firstValueFrom(ref.closed);
  if (!confirmed) return;
  await firstValueFrom(
    this.sessionService.updateStatus(this.sessionState.currentSessionId()!, { action: 'end' })
  );
}
```

## Template Layout
```html
<!-- Only render when visible() -->
@if (visible()) {
  <div class="collab-toolbar" cdkDrag cdkDragBoundary="body">
    <!-- Connection indicator + session code -->
    <div class="collab-toolbar__session-info">
      <span class="collab-toolbar__dot" [class.reconnecting]="isReconnecting()"></span>
      <span class="collab-toolbar__code">{{ sessionCode() }}</span>
      <span class="collab-toolbar__count">{{ participantCount() }} live</span>
    </div>

    <div class="collab-toolbar__divider"></div>

    <!-- Mode buttons -->
    <button class="collab-toolbar__btn" [class.active]="!isScreenSharing()" title="Co-Browse">🌐</button>
    <button class="collab-toolbar__btn" [class.active]="isScreenSharing()"
            [class.danger]="isScreenSharing()" (click)="toggleScreenShare()" title="Screen Share">📺</button>

    <div class="collab-toolbar__divider"></div>

    <!-- Action buttons -->
    <button class="collab-toolbar__btn" [class.active]="isChatOpen()"
            (click)="uiState.toggleChat()" title="Chat">
      💬
      @if (chatBadge() > 0) { <span class="collab-toolbar__badge">{{ chatBadge() }}</span> }
    </button>
    <button class="collab-toolbar__btn" [class.active]="isAnnotating()"
            (click)="uiState.toggleAnnotateMode()" title="Annotate">✏️</button>
    <button class="collab-toolbar__btn" (click)="uiState.openDetailPanel()" title="Participants">👥</button>

    <div class="collab-toolbar__divider"></div>

    <!-- Timer + End -->
    <span class="collab-toolbar__timer">{{ elapsed() }}</span>
    <button class="collab-toolbar__btn collab-toolbar__btn--end" (click)="endSession()" title="End Session">☎</button>
  </div>
}
```

## SCSS
```scss
// Position: fixed bottom-24px left-50% translateX(-50%)
// z-index: var(--collab-z-toolbar) [=1000]
// background: rgba(14,21,32,0.92) backdrop-filter:blur(20px)
// border: 1px solid rgba(255,255,255,0.08)
// border-radius: 50px  padding: 8px 16px
// Entry: @keyframes collabSlideUp — slide up 20px + fade in 0.3s
// Exit: reverse
// Drag handle cursor on session-info area
// .btn: 38px circle, hover: bg rgba(255,255,255,0.08)
// .btn.active: bg var(--collab-teal-glow) color var(--collab-teal)
// .btn.danger: bg var(--collab-red-dim) color var(--collab-red)
// .btn--end: always danger colors
// .badge: absolute top-0 right-0 teal circle 14px mono font
// .dot: 6px green circle, .reconnecting → amber + pulse animation
// .timer: font-mono 13px color var(--collab-teal)
// .divider: 1px vertical line rgba(255,255,255,0.1) height 20px
```

---
---

# PROMPT 09 — Session Dashboard Page
## Prerequisites: Prompts 01–07 complete

---

## Your Task
Implement `pages/dashboard/collab-dashboard.component.ts + .html`
Main dashboard showing active sessions, scheduled, stats, pending invites.
See `@reference/collab_prototype.html` for layout reference.

---

```typescript
@Component({ standalone: true, changeDetection: ChangeDetectionStrategy.OnPush })
export class CollabDashboardComponent implements OnInit {
  private readonly sessionService = inject(CollabSessionService);
  private readonly inviteService  = inject(CollabInviteService);
  private readonly sessionState   = inject(SessionState);
  private readonly uiState        = inject(CollabUiState);
  private readonly router         = inject(Router);

  // Load on init via toSignal():
  readonly activeSessions   = toSignal(this.sessionService.getActiveSessions(),   { initialValue: [] });
  readonly scheduledSessions = toSignal(this.sessionService.getScheduledSessions(), { initialValue: [] });
  readonly pendingInvites   = toSignal(this.inviteService.getPendingInvites(),    { initialValue: [] });
  readonly isLoading        = this.sessionState.isLoading;

  // Computed:
  readonly scheduledToday = computed(() =>
    this.scheduledSessions().filter(s => s.scheduledAt && isToday(s.scheduledAt))
  );
  readonly sessionsDueSoon = this.sessionState.sessionsDueSoon;

  // Actions:
  joinSession(sessionId: string): void    // sessionService.joinSession() → navigate
  viewSession(sessionId: string): void    // uiState.openDetailPanel(), set selected
  openInviteModal(sessionId: string): void // uiState.openInviteModal(sessionId)
  startScheduled(sessionId: string): void // update status to active
  acceptInvite(inviteId: string): void    // inviteService.respondToInvite(id, true)
  declineInvite(inviteId: string): void   // inviteService.respondToInvite(id, false)
  navigateToCreate(): void               // router.navigate(['/collaboration/new'])
}
```

## Template Structure

```html
<!-- Page header with title + "New Session" button -->

<!-- Live session banner — @if (sessionState.hasActiveSession()) -->
<!-- Shows: title, mode, participant count, live timer, View + End buttons -->

<!-- 4-column stats row -->
<!-- [Active Sessions] [Scheduled Today] [Pending Invites] [This Week] -->

<!-- Active sessions grid using SessionCardComponent -->
<!-- @for with empty state fallback -->

<!-- Scheduled sessions table -->
<!-- Columns: Session | Module | Scheduled At | Participants | Actions -->
<!-- "Start Now" button highlighted if isDueSoon -->

<!-- Pending invites section (only if pendingInvites().length > 0) -->
<!-- Invite cards with Accept + Decline -->

<!-- Loading skeleton overlay @if (isLoading()) -->
```

---
---

# PROMPT 10 — Create Session Form
## Prerequisites: Prompts 01–07 complete

---

## Your Task
Implement `pages/session-create/session-create.component.ts + .html`
Works as both a full page (`/collaboration/new`) and as a CDK Dialog.

---

```typescript
// Form:
readonly form = inject(FormBuilder).group({
  title:                    ['', [Validators.required, Validators.maxLength(200)]],
  sessionType:              [SessionType.AdHoc, Validators.required],
  mode:                     [SessionMode.CoBrowse, Validators.required],
  contextModuleCode:        [null as string | null],
  contextRecordId:          [null as string | null],
  contextRecordLabel:       [null as string | null],
  maxParticipants:          [null as number | null, [Validators.min(2), Validators.max(100)]],
  scheduledAt:              [null as string | null],
  scheduledDurationMinutes: [60],
});

// When sessionType changes to Scheduled: add required validator to scheduledAt
// When sessionType changes to AdHoc: remove it
// Use form.get('sessionType').valueChanges with effect() or subscription
```

## Template Structure
```html
<!-- Section 1: Session Type — two card buttons (Ad-hoc / Scheduled) -->
<!-- @if (form.get('sessionType').value === 'Scheduled') { scheduled fields } -->

<!-- Section 2: Mode — three card buttons (Co-Browse / Screen Share / Hybrid) -->
<!-- Each card has icon + label + brief description -->

<!-- Section 3: Module Context (optional) -->
<!-- Module select dropdown + Record reference input -->
<!-- Placeholder text changes based on selected module -->

<!-- Section 4: Advanced (collapsible) -->
<!-- Max participants input + description field -->

<!-- Footer: Cancel button + Submit button with loading spinner -->

<!-- Form error summary if submitted invalid -->
```

## Submit
```typescript
readonly isSubmitting = signal(false);

async submit(): Promise<void> {
  if (this.form.invalid) { this.form.markAllAsTouched(); return; }
  this.isSubmitting.set(true);
  try {
    const req = this.buildRequest();
    await firstValueFrom(this.sessionService.createSession(req));
    if (req.sessionType === SessionType.AdHoc) {
      this.router.navigate(['/collaboration']);
      this.uiState.showToast({ type: 'success', message: 'Session started!' });
    } else {
      this.router.navigate(['/collaboration']);
      this.uiState.showToast({ type: 'success', message: `Scheduled: ${req.title}` });
    }
  } finally {
    this.isSubmitting.set(false);
  }
}
```

---
---

# PROMPT 11 — Session Detail Side Panel
## Prerequisites: Prompts 01–07 complete

---

## Your Task
Implement `components/detail-panel/detail-panel.component.ts + .html`
Slide-in panel showing session details + participant management.

---

```typescript
@Component({ standalone: true, changeDetection: ChangeDetectionStrategy.OnPush })
export class DetailPanelComponent {
  private readonly uiState        = inject(CollabUiState);
  private readonly sessionState   = inject(SessionState);
  private readonly participantState = inject(ParticipantState);
  private readonly sessionService = inject(CollabSessionService);
  private readonly dialog         = inject(Dialog);

  // From state:
  readonly isOpen       = this.uiState.detailPanelOpen;
  readonly session      = this.sessionState.currentSession;
  readonly myUserId     = this.participantState.myUserId;
  readonly isHost       = this.participantState.isHost;
  readonly controlReq   = this.uiState.controlRequest;

  // Sorted participants: Host first, then by role priority, then joinedAt
  readonly sortedParticipants = computed(() => {
    const p = this.session()?.participants ?? [];
    return [...p].sort((a, b) =>
      getRolePriority(a.role) - getRolePriority(b.role) ||
      a.joinedAt.getTime() - b.joinedAt.getTime()
    );
  });

  readonly elapsed = computed(() => {
    const s = this.session()?.startedAt;
    return s ? formatElapsed(Date.now() - s.getTime()) : '—';
  });
}
```

## Template Structure
```html
<!-- Slide-in overlay: transform translateX based on isOpen() -->
<!-- Click outside (backdrop div) → closeDetailPanel() -->
<!-- ESC key → closeDetailPanel() via HostListener -->

<!-- Panel header: title, code (mono), status pill, close X -->

<!-- Section: Session Info grid -->
<!-- Code | Mode | Module | Record | Duration | Max Participants -->

<!-- Section: Control Request Banner (if controlRequest() !== null) -->
<!-- "[Name] is requesting control" + Accept + Deny buttons -->

<!-- Section: Participants (@for sortedParticipants) -->
<!-- Each row: avatar | name + "You" badge | role badge | hasControl indicator -->
<!-- Host actions (only show if isHost()): Give Control | Change Role | Remove -->

<!-- Section: Invite -->
<!-- "Add participants" button → openInviteModal -->

<!-- Panel footer: End Session danger button -->
```

## Host Actions
```typescript
async grantControl(targetUserId: string): Promise<void> {
  const sessionId = this.sessionState.currentSessionId()!;
  await firstValueFrom(this.sessionService.grantControl(sessionId, targetUserId));
}

async acceptControlRequest(): Promise<void> {
  const req = this.controlRequest();
  if (!req) return;
  await this.grantControl(req.userId);
  this.uiState.clearControlRequest();
}

closePanel(): void { this.uiState.closeDetailPanel(); }
```

## SCSS
```scss
// Panel: position fixed right-0 top-0 height-100vh width-480px
// transform: translateX(0) when open, translateX(100%) when closed
// transition: transform 0.3s cubic-bezier(0.4,0,0.2,1)
// backdrop div: position fixed inset-0 bg rgba(0,0,0,0) — does NOT close panel
// z-index: var(--collab-z-overlay) [=200]
// Sections separated by 1px border
// Scrollable body with custom scrollbar
// Avatar circles: 32px, bg = cursorColor at 15% opacity, color = cursorColor
// "You" badge: tiny teal pill next to name
// Role badge: .collab-role-badge--{role} classes
// Has control indicator: ✎ pencil icon in teal next to name
// Control request banner: amber background, stands out
```

---
---

# PROMPT 12 — Chat Panel Component
## Prerequisites: Prompts 01–07, 08 complete (toolbar)

---

## Your Task
Implement `components/chat-panel/chat-panel.component.ts + .html`

---

```typescript
@Component({ standalone: true, changeDetection: ChangeDetectionStrategy.OnPush })
export class ChatPanelComponent implements OnInit {
  private readonly hub     = inject(CollabHubService);
  private readonly uiState = inject(CollabUiState);
  private readonly sessionState = inject(SessionState);
  private readonly participantState = inject(ParticipantState);
  private readonly destroyRef = inject(DestroyRef);

  private readonly _messages = signal<ChatMessage[]>([]);
  readonly messages  = this._messages.asReadonly();
  readonly isOpen    = this.uiState.chatOpen;
  readonly inputText = signal('');
  readonly charCount = computed(() => this.inputText().length);
  readonly isOverLimit = computed(() => this.charCount() > COLLAB_MAX_CHAT_LENGTH);

  @ViewChild('messagesContainer') messagesContainer!: ElementRef;

  ngOnInit(): void {
    // Subscribe to hub.chatMessageReceived$
    // Add to _messages, sort by sentAt
    // If chat is NOT open → uiState.incrementUnreadChat()
    // Auto-scroll after new message: scrollToBottom()
  }

  async sendMessage(): Promise<void> {
    const text = this.inputText().trim();
    if (!text || this.isOverLimit()) return;
    const sessionId = untracked(() => this.sessionState.currentSessionId()!);
    // Optimistic add:
    const optimistic: ChatMessage = { sessionId, fromUserId: 'me', fromDisplayName: 'You',
      cursorColor: '#3182CE', text, sentAt: new Date() };
    this._messages.update(m => [...m, optimistic]);
    this.inputText.set('');
    this.scrollToBottom();
    try {
      await this.hub.sendChatMessage(sessionId, text);
    } catch {
      // Remove optimistic message on failure
      this._messages.update(m => m.filter(msg => msg !== optimistic));
    }
  }

  private scrollToBottom(): void {
    setTimeout(() => {
      const el = this.messagesContainer?.nativeElement;
      if (el) el.scrollTop = el.scrollHeight;
    }, 0);
  }
}
```

## Template
```html
<!-- Floating panel: fixed bottom-80px right-20px width-300px height-380px -->
<!-- Visibility: opacity + transform based on isOpen() -->

<!-- Header: "💬 Session Chat" + participant count + X close -->

<!-- Messages area (scrollable, #messagesContainer): -->
<!-- @for (msg of messages(); track msg.sentAt) -->
<!--   @if own message: right-aligned teal bubble -->
<!--   @if other: left-aligned with avatar + sender name above bubble -->

<!-- Input area: -->
<!-- Text input (Enter=send, Shift+Enter=newline) + send button -->
<!-- Char counter (show only when > 800 chars used): "180 / 1000" -->
```

## SCSS
```scss
// Position: fixed right-20px, above toolbar (bottom: 80px)
// Entry: translateY(10px) + scale(0.97) → translateY(0) + scale(1) when open
// Own bubble: border-radius 12px 2px 12px 12px, bg teal-glow, color teal
// Other bubble: border-radius 2px 12px 12px 12px, bg bg-hover
// Avatar: 22px circle using cursorColor
// Timestamps: hidden, show on .chat-message:hover (CSS only)
```

---
---

# PROMPT 13 — Remote Cursor Overlay
## Prerequisites: Prompts 01–07 complete

---

## Your Task
Implement `components/cursor-overlay/cursor-overlay.component.ts + .html`
Renders other participants' cursors on top of the entire app.
MUST NOT block any user interaction.

---

```typescript
@Component({
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: {
    style: 'position:fixed;inset:0;pointer-events:none;z-index:999;overflow:hidden'
  }
})
export class CursorOverlayComponent implements OnInit {
  private readonly hub              = inject(CollabHubService);
  private readonly participantState = inject(ParticipantState);
  private readonly sessionState     = inject(SessionState);
  private readonly destroyRef       = inject(DestroyRef);

  readonly isActive    = this.sessionState.hasActiveSession;
  readonly otherCursors = this.participantState.otherPresence;

  ngOnInit(): void {
    // Subscribe to hub.cursorMoved$
    // Call participantState.updateCursor() on each event
    // The computed otherPresence signal then drives the template
  }
}
```

## Template
```html
@if (isActive()) {
  @for (cursor of otherCursors(); track cursor.userId) {
    @if (cursor.isOnline && cursor.cursorX > 0) {
      <div class="cursor-label"
           [style.left.%]="cursor.cursorX"
           [style.top.%]="cursor.cursorY"
           [style.--cursor-color]="cursor.cursorColor">
        <span class="cursor-dot"></span>
        <span class="cursor-name">{{ cursor.displayName.split(' ')[0] }}</span>
      </div>
    }
  }
}
```

## SCSS
```scss
// .cursor-label:
//   position: absolute
//   transform: translate(4px, 4px)
//   transition: left 0.08s linear, top 0.08s linear
//   display: flex align-items: center gap: 4px
//   padding: 2px 7px border-radius: 4px
//   background: rgba(var(--cursor-color), 0.15)
//   border: 1px solid rgba(var(--cursor-color), 0.35)
//   color: var(--cursor-color)  — note: use CSS var trick for color
//   font: 11px DM Mono
//   pointer-events: none  ← critical
//   white-space: nowrap
//
// .cursor-dot: 5px circle background currentColor
//
// IMPORTANT: component host has pointer-events: none
// Individual labels also pointer-events: none
```

## Note on color CSS vars
```typescript
// Since CSS rgba() can't take a full hex, use:
// Pass color as a CSS custom property in the style binding
// [style]="{ '--cursor-color': cursor.cursorColor }"
// Then in SCSS use color: var(--cursor-color) directly without rgba()
// For background use: background: color-mix(in srgb, var(--cursor-color) 15%, transparent)
```

---
---

# PROMPT 14 — Invite Flow
## Prerequisites: Prompts 01–07 complete

---

## Your Task
Implement `components/invite-modal/invite-modal.component.ts + .html`
Handles both user invite and link generation in two tabs.

---

```typescript
@Component({ standalone: true, changeDetection: ChangeDetectionStrategy.OnPush })
export class InviteModalComponent {
  readonly dialogRef    = inject(DialogRef);
  readonly data         = inject<{ sessionId: string; sessionTitle: string }>(DIALOG_DATA);
  private readonly inviteService = inject(CollabInviteService);

  readonly activeTab    = signal<'user' | 'link'>('user');
  readonly selectedRole = signal<ParticipantRole>(ParticipantRole.Viewer);
  readonly selectedUser = signal<string | null>(null);
  readonly generatedInvite = signal<CollabSessionInvite | null>(null);
  readonly isSending    = signal(false);
  readonly isGenerating = signal(false);
  readonly linkCopied   = signal(false);

  // User list would come from an injected UserService in real app
  // For this component, accept as input or use a simple mock observable

  async sendUserInvite(): Promise<void> {
    if (!this.selectedUser()) return;
    this.isSending.set(true);
    await firstValueFrom(
      this.inviteService.inviteUsers(this.data.sessionId, {
        userIds: [this.selectedUser()!],
        offeredRole: this.selectedRole(),
      })
    );
    this.isSending.set(false);
    // Show success inline, reset form
  }

  async generateLink(): Promise<void> {
    this.isGenerating.set(true);
    const invite = await firstValueFrom(
      this.inviteService.createLinkInvite(this.data.sessionId, {
        offeredRole: this.selectedRole(),
      })
    );
    this.generatedInvite.set(invite);
    this.isGenerating.set(false);
  }

  copyLink(): void {
    const url = this.generatedInvite()?.joinUrl;
    if (!url) return;
    navigator.clipboard.writeText(url);
    this.linkCopied.set(true);
    setTimeout(() => this.linkCopied.set(false), 2000);
  }
}
```

## Template Structure
```html
<!-- Modal header: "Invite Participants" + session title + close X -->

<!-- Tab switcher: [Invite by User] [Share Link] -->

<!-- Tab: Invite by User -->
<!-- User select/typeahead + Role dropdown + Send button -->
<!-- Success message inline after send -->

<!-- Tab: Share Link -->
<!-- Role dropdown -->
<!-- Generate button → shows link in copyable input -->
<!-- Copy button with "Copied!" feedback state -->
<!-- Expiry time shown -->

<!-- Footer: Close button -->
```

---
---

# PROMPT 15 — Session Card Component
## Prerequisites: Prompts 01–03 complete (models + state)

---

## Your Task
Implement `components/session-card/session-card.component.ts + .html`
Reusable card for displaying a session in grids and lists.

---

```typescript
@Component({
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: 'collab-session-card',
})
export class SessionCardComponent {
  @Input({ required: true }) session!: CollabSession | CollabSessionSummary;
  @Input() isCurrentSession = false;
  @Input() displayMode: 'grid' | 'compact' = 'grid';

  @Output() join    = new EventEmitter<string>();
  @Output() invite  = new EventEmitter<string>();
  @Output() view    = new EventEmitter<string>();
  @Output() start   = new EventEmitter<string>();

  readonly modeIcon  = computed(() => getModeIcon(this.session.mode));
  readonly modeLabel = computed(() => getModeLabel(this.session.mode));
  readonly isDue     = computed(() => {
    const s = this.session;
    if (!('scheduledAt' in s) || !s.scheduledAt) return false;
    const diff = s.scheduledAt.getTime() - Date.now();
    return diff >= 0 && diff <= 15 * 60 * 1000;
  });

  // Max 4 participant avatars + "+N" overflow
  readonly visibleParticipants = computed(() => {
    const p = 'participants' in this.session ? this.session.participants : [];
    return { shown: p.slice(0, 4), overflow: Math.max(0, p.length - 4) };
  });
}
```

## Template
```html
<!-- Card container: hover → teal border + shadow + translateY(-2px) -->
<!-- Active session variant: subtle teal gradient background -->

<!-- Top row: mode icon (colored circle) + status pill -->
<!-- Title (2-line clamp) -->
<!-- Meta row: code · mode · elapsed/scheduled time -->
<!-- Context chip: @if contextModuleCode: "AP · Invoice #4521" -->
<!-- Participants: overlapping avatars + overflow count -->
<!-- Action buttons: conditional on status -->
<!--   Active: Join (primary) + Invite (secondary) -->
<!--   Scheduled: @if isDue → "Start Now" (primary highlighted) else time display -->
```

## SCSS
```scss
// Card: bg-card border rounded-12px padding-18px transition-all
// hover: border-color teal-dim, box-shadow teal-glow, translateY(-2px)
// .active-session: border-color rgba(teal,0.3), radial gradient bg overlay
// .status-pill: font-mono uppercase 10px — color coded per status
// .context-chip: inline-flex gap-6px font-mono 11px bg-hover rounded padding-6px-10px
// .mode-icon: 38px circle bg per mode (cobrowse=teal, screenshare=blue, hybrid=purple)
// Participant avatars: 22px circles, margin-left -6px, border 2px bg-card
// Title: webkit-line-clamp 2, overflow hidden
```

---
---

# PROMPT 16 — Org Configuration Page
## Prerequisites: Prompts 01–07 complete

---

## Your Task
Implement `pages/config/collab-config.component.ts + .html`
Admin page to configure per-org collaboration settings.

---

```typescript
@Component({ standalone: true, changeDetection: ChangeDetectionStrategy.OnPush })
export class CollabConfigComponent implements OnInit {
  private readonly configService = inject(CollabConfigService);
  private readonly fb = inject(FormBuilder);
  private readonly uiState = inject(CollabUiState);

  readonly isLoading = signal(true);
  readonly isSaving  = signal(false);
  readonly lastSaved = signal<Date | null>(null);

  readonly form = this.fb.group({
    limits: this.fb.group({
      maxParticipantsDefault:      [10,  [Validators.required, Validators.min(2),  Validators.max(100)]],
      maxParticipantsAbsolute:     [25,  [Validators.required, Validators.min(2),  Validators.max(100)]],
      sessionTimeoutMinutes:       [30,  [Validators.required, Validators.min(5),  Validators.max(480)]],
      inviteLinkExpiryMinutes:     [60,  [Validators.required, Validators.min(10), Validators.max(1440)]],
      maxScheduledSessionsPerUser: [20,  [Validators.required, Validators.min(1),  Validators.max(100)]],
    }),
    features: this.fb.group({
      allowScreenShare:      [true], allowAnonymousJoin:    [false],
      allowGuestJoinViaLink: [true], enableAuditLog:        [true],
      enableCoBrowse:        [true], enableInSessionChat:   [true],
      enableAnnotations:     [true], enableCursorBroadcast: [true],
      enableFormSync:        [true],
    }),
    controlConsentMode: [ControlConsentMode.RoleDependent],
    allowedModules: [null as string[] | null],
  });

  // Cross validation: maxDefault must be <= maxAbsolute
  // Show inline warning (not error) when violated — don't block save

  ngOnInit(): void {
    this.configService.getConfig().subscribe(config => {
      this.form.patchValue({ /* map config to form structure */ });
      this.isLoading.set(false);
    });
  }

  async save(): Promise<void> {
    if (this.form.invalid) { this.form.markAllAsTouched(); return; }
    this.isSaving.set(true);
    const req = this.buildRequest();
    await firstValueFrom(this.configService.updateConfig(req));
    this.lastSaved.set(new Date());
    this.form.markAsPristine();
    this.uiState.showToast({ type: 'success', message: 'Configuration saved' });
    this.isSaving.set(false);
  }

  resetToDefaults(): void {
    // Patch form with DEFAULT_COLLAB_CONFIG values
  }
}
```

## Template Structure
```html
<!-- Page header + Save button + "last saved" timestamp -->

<!-- Unsaved changes banner: @if (form.dirty && !isSaving()) -->

<!-- Two-column layout (desktop) / single (mobile): -->

<!-- LEFT: Limits card -->
<!-- All inputs with min/max hints below -->
<!-- Warning when maxDefault > maxAbsolute -->

<!-- RIGHT: Features card -->
<!-- Toggle switches for each boolean -->
<!-- Three groups: Core | Security | Advanced -->
<!-- Each toggle: label + description text + switch -->

<!-- FULL WIDTH: Control Consent Mode -->
<!-- 3 radio cards: Not Required | Always Required | Role Dependent -->
<!-- Selected card highlighted teal -->

<!-- FULL WIDTH: Module Restrictions -->
<!-- "All modules" radio vs specific selection -->
<!-- Multi-chip input for module codes -->

<!-- Loading skeleton: @if (isLoading()) -->
```
