# nodes

id: example:examples/auth-form.html
type: example
name: auth-form.html
path: examples/auth-form.html
data: {}
body:
<!doctype html>
<html lang="en" data-theme="default">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>RenDS · Auth form example</title>

  <!-- Foundation: tokens, reset, classless, utilities, layouts -->
  <link rel="stylesheet" href="../index.css">

  <!-- Component CSS (use components/index.css if importing many) -->
  <link rel="stylesheet" href="../components/primitives/ren-card/ren-card.css">
  <link rel="stylesheet" href="../components/primitives/ren-field/ren-field.css">
  <link rel="stylesheet" href="../components/primitives/ren-button/ren-button.css">
  <link rel="stylesheet" href="../components/primitives/ren-link/ren-link.css">
  <link rel="stylesheet" href="../components/primitives/ren-separator/ren-separator.css">
  <link rel="stylesheet" href="../components/patterns/ren-form/ren-form.css">

  <!-- Component JS -->
  <script type="module" src="../components/primitives/ren-field/ren-field.js"></script>
  <script type="module" src="../components/patterns/ren-form/ren-form.js"></script>
</head>
<body>
  <!--
    Layout skeleton: ren-cover centers a full-viewport stack with optional
    top / bottom regions. The middle child is the centered surface.
  -->
  <div class="ren-cover">
    <header></header>

    <main class="ren-center-narrow">
      <article class="ren-card ren-stack-lg">
        <header class="ren-stack-sm">
          <h1 class="ren-card-title">Sign in to Acme</h1>
          <p class="ren-card-description">Welcome back. Enter your credentials to continue.</p>
        </header>

        <ren-form data-validate="onTouched">
          <form class="ren-form ren-stack" novalidate>
            <div class="ren-form-error-summary" role="alert" tabindex="-1" hidden>
              <strong>Please fix the following errors:</strong>
              <ul></ul>
            </div>

            <ren-field data-rules="required|email">
              <label>Email</label>
              <input class="ren-input" type="email" name="email" autocomplete="email" required>
              <span data-error></span>
            </ren-field>

            <ren-field data-rules="required|min:8">
              <label>Password</label>
              <input class="ren-input" type="password" name="password" autocomplete="current-password" required minlength="8">
              <span data-description>At least 8 characters.</span>
              <span data-error></span>
            </ren-field>

            <div class="ren-row-spread">
              <label class="ren-row" style="gap: var(--space-2)">
                <input type="checkbox" name="remember">
                <span>Remember me</span>
              </label>
              <a class="ren-link" href="/forgot">Forgot password?</a>
            </div>

            <div class="ren-form-actions">
              <button class="ren-btn ren-btn-full" type="submit">Sign in</button>
            </div>
          </form>
        </ren-form>

        <hr class="ren-separator" aria-hidden="true">

        <a class="ren-btn ren-btn-secondary ren-btn-full" href="/auth/google">
          Continue with Google
        </a>

        <p class="ren-stack-xs" style="text-align: center; color: var(--color-text-muted)">
          Don't have an account?
          <a class="ren-link" href="/signup">Create one</a>
        </p>
      </article>
    </main>

    <footer></footer>
  </div>

  <script type="module">
    document.querySelector('ren-form').addEventListener('ren-submit', (event) => {
      event.preventDefault();
      // POST event.detail.values to your auth endpoint here.
      console.log('submit', event.detail.values);
    });
  </script>
</body>
</html>


id: example:examples/dashboard-shell.html
type: example
name: dashboard-shell.html
path: examples/dashboard-shell.html
data: {}
body:
<!doctype html>
<html lang="en" data-theme="default">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>RenDS · Dashboard shell example</title>

  <link rel="stylesheet" href="../index.css">
  <link rel="stylesheet" href="../components/index.css">

  <script type="module" src="../components/patterns/ren-sidebar/ren-sidebar.js"></script>
  <script type="module" src="../components/primitives/ren-button/ren-button.js"></script>
  <script type="module" src="../components/primitives/ren-field/ren-field.js"></script>
</head>
<body>
  <!--
    App shell: ren-sidebar-layout = sidebar (left) + main (right).
    Sidebar persists across routes; main hosts the page-level content.
    Layout uses ren-stack-lg to space large sections inside main.
  -->
  <div class="ren-sidebar-layout">
    <ren-sidebar class="ren-sidebar" aria-label="Primary">
      <div class="ren-sidebar-header">
        <a class="ren-sidebar-item" href="/" aria-label="Acme home">
          <span class="ren-sidebar-item-icon" aria-hidden="true">⌂</span>
          <span class="ren-sidebar-item-text">Acme</span>
        </a>
      </div>

      <nav class="ren-sidebar-nav" aria-label="Main">
        <a class="ren-sidebar-item active" href="/dashboard" aria-current="page">
          <span class="ren-sidebar-item-icon" aria-hidden="true">📊</span>
          <span class="ren-sidebar-item-text">Dashboard</span>
        </a>
        <a class="ren-sidebar-item" href="/projects">
          <span class="ren-sidebar-item-icon" aria-hidden="true">📁</span>
          <span class="ren-sidebar-item-text">Projects</span>
        </a>
        <a class="ren-sidebar-item" href="/team">
          <span class="ren-sidebar-item-icon" aria-hidden="true">👥</span>
          <span class="ren-sidebar-item-text">Team</span>
        </a>
        <a class="ren-sidebar-item" href="/billing">
          <span class="ren-sidebar-item-icon" aria-hidden="true">💳</span>
          <span class="ren-sidebar-item-text">Billing</span>
        </a>
      </nav>

      <div class="ren-sidebar-section">
        <div class="ren-sidebar-section-label">Account</div>
        <a class="ren-sidebar-item" href="/settings">
          <span class="ren-sidebar-item-icon" aria-hidden="true">⚙</span>
          <span class="ren-sidebar-item-text">Settings</span>
        </a>
      </div>

      <button class="ren-sidebar-toggle" type="button" aria-label="Toggle sidebar"></button>
    </ren-sidebar>

    <main class="ren-sidebar-main">
      <div class="ren-center-wide ren-stack-lg" style="padding: var(--space-6)">
        <!-- Page header -->
        <header class="ren-row-spread">
          <div class="ren-stack-xs">
            <h1>Dashboard</h1>
            <p style="color: var(--color-text-muted)">Welcome back, Renato.</p>
          </div>
          <div class="ren-cluster">
            <button class="ren-btn ren-btn-secondary" type="button">Export</button>
            <button class="ren-btn" type="button">New project</button>
          </div>
        </header>

        <!-- Stat tiles -->
        <section class="ren-grid-3" aria-label="Key metrics">
          <article class="ren-card">
            <div class="ren-card-header">
              <p class="ren-card-description">Active users</p>
            </div>
            <div class="ren-card-body ren-row-spread">
              <strong style="font-size: var(--text-4xl)">12,438</strong>
              <span class="ren-badge ren-badge-success">+8.2%</span>
            </div>
          </article>

          <article class="ren-card">
            <div class="ren-card-header">
              <p class="ren-card-description">Revenue (MRR)</p>
            </div>
            <div class="ren-card-body ren-row-spread">
              <strong style="font-size: var(--text-4xl)">$48.2k</strong>
              <span class="ren-badge ren-badge-success">+3.1%</span>
            </div>
          </article>

          <article class="ren-card">
            <div class="ren-card-header">
              <p class="ren-card-description">Churn</p>
            </div>
            <div class="ren-card-body ren-row-spread">
              <strong style="font-size: var(--text-4xl)">1.4%</strong>
              <span class="ren-badge ren-badge-danger">+0.3%</span>
            </div>
          </article>
        </section>

        <!-- Recent activity -->
        <section class="ren-card ren-stack">
          <header class="ren-card-header ren-row-spread">
            <h2 class="ren-card-title">Recent projects</h2>
            <a class="ren-link" href="/projects">View all</a>
          </header>
          <div class="ren-card-body">
            <table class="ren-table">
              <caption class="ren-sr-only">Recent projects</caption>
              <thead>
                <tr><th scope="col">Name</th><th scope="col">Owner</th><th scope="col">Status</th><th scope="col">Updated</th></tr>
              </thead>
              <tbody>
                <tr><td>Atlas</td><td>Renato</td><td><span class="ren-badge ren-badge-success">Active</span></td><td>2m ago</td></tr>
                <tr><td>Mercury</td><td>Sofía</td><td><span class="ren-badge ren-badge-warning">Paused</span></td><td>1h ago</td></tr>
                <tr><td>Nimbus</td><td>Iván</td><td><span class="ren-badge">Draft</span></td><td>Yesterday</td></tr>
              </tbody>
            </table>
          </div>
        </section>
      </div>
    </main>
  </div>
</body>
</html>


id: example:examples/data-table.html
type: example
name: data-table.html
path: examples/data-table.html
data: {}
body:
<!doctype html>
<html lang="en" data-theme="default">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>RenDS · Data table example</title>

  <link rel="stylesheet" href="../index.css">
  <link rel="stylesheet" href="../components/index.css">

  <script type="module" src="../components/primitives/ren-field/ren-field.js"></script>
</head>
<body>
  <!--
    List management screen: filters at the top, table below, pagination
    underneath. ren-center caps width, ren-stack-lg spaces sections.
  -->
  <main class="ren-center-wide ren-stack-lg" style="padding: var(--space-6)">
    <header class="ren-row-spread">
      <div class="ren-stack-xs">
        <h1>Users</h1>
        <p style="color: var(--color-text-muted)">3,482 active accounts.</p>
      </div>
      <div class="ren-cluster">
        <button class="ren-btn ren-btn-secondary" type="button">Export CSV</button>
        <button class="ren-btn" type="button">Invite user</button>
      </div>
    </header>

    <!-- Filters -->
    <section class="ren-card">
      <div class="ren-card-body ren-row" style="gap: var(--space-3); flex-wrap: wrap">
        <ren-field style="flex: 1; min-width: 12rem">
          <label class="ren-sr-only">Search users</label>
          <span class="ren-input-wrapper">
            <span class="ren-input-icon" aria-hidden="true">🔍</span>
            <input class="ren-input" type="search" name="q" placeholder="Search by name or email">
          </span>
        </ren-field>

        <ren-field>
          <label class="ren-sr-only">Filter role</label>
          <select class="ren-input" name="role">
            <option>All roles</option>
            <option>Admin</option>
            <option>Member</option>
            <option>Billing</option>
          </select>
        </ren-field>

        <ren-field>
          <label class="ren-sr-only">Filter status</label>
          <select class="ren-input" name="status">
            <option>All statuses</option>
            <option>Active</option>
            <option>Pending</option>
            <option>Suspended</option>
          </select>
        </ren-field>
      </div>
    </section>

    <!-- Table -->
    <section class="ren-card">
      <table class="ren-table">
        <caption class="ren-sr-only">Users</caption>
        <thead>
          <tr>
            <th scope="col" style="width: 2rem">
              <label class="ren-checkbox">
                <input type="checkbox" aria-label="Select all">
                <span class="ren-checkbox-control"></span>
              </label>
            </th>
            <th scope="col">Name</th>
            <th scope="col">Email</th>
            <th scope="col">Role</th>
            <th scope="col">Status</th>
            <th scope="col">Last seen</th>
            <th scope="col"><span class="ren-sr-only">Actions</span></th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td>
              <label class="ren-checkbox">
                <input type="checkbox" aria-label="Select Sofía">
                <span class="ren-checkbox-control"></span>
              </label>
            </td>
            <td>
              <div class="ren-row" style="gap: var(--space-2)">
                <span class="ren-avatar" aria-hidden="true"><span>S</span></span>
                <span>Sofía Reyes</span>
              </div>
            </td>
            <td>sofia@acme.io</td>
            <td><span class="ren-tag ren-tag-primary">Admin</span></td>
            <td><span class="ren-badge ren-badge-success">Active</span></td>
            <td>2m ago</td>
            <td>
              <button class="ren-btn ren-btn-ghost ren-btn-sm" type="button" aria-label="More actions">⋯</button>
            </td>
          </tr>
          <tr>
            <td>
              <label class="ren-checkbox">
                <input type="checkbox" aria-label="Select Iván">
                <span class="ren-checkbox-control"></span>
              </label>
            </td>
            <td>
              <div class="ren-row" style="gap: var(--space-2)">
                <span class="ren-avatar" aria-hidden="true"><span>I</span></span>
                <span>Iván Castro</span>
              </div>
            </td>
            <td>ivan@acme.io</td>
            <td><span class="ren-tag">Member</span></td>
            <td><span class="ren-badge ren-badge-warning">Pending</span></td>
            <td>1h ago</td>
            <td>
              <button class="ren-btn ren-btn-ghost ren-btn-sm" type="button" aria-label="More actions">⋯</button>
            </td>
          </tr>
          <tr>
            <td>
              <label class="ren-checkbox">
                <input type="checkbox" aria-label="Select Lía">
                <span class="ren-checkbox-control"></span>
              </label>
            </td>
            <td>
              <div class="ren-row" style="gap: var(--space-2)">
                <span class="ren-avatar" aria-hidden="true"><span>L</span></span>
                <span>Lía Vargas</span>
              </div>
            </td>
            <td>lia@acme.io</td>
            <td><span class="ren-tag">Member</span></td>
            <td><span class="ren-badge ren-badge-danger">Suspended</span></td>
            <td>3d ago</td>
            <td>
              <button class="ren-btn ren-btn-ghost ren-btn-sm" type="button" aria-label="More actions">⋯</button>
            </td>
          </tr>
        </tbody>
      </table>
    </section>

    <!-- Pagination -->
    <nav class="ren-pagination ren-row-spread" aria-label="Pagination">
      <span style="color: var(--color-text-muted)">Showing 1–3 of 3,482</span>
      <div class="ren-cluster">
        <button class="ren-btn ren-btn-ghost" type="button" disabled>Previous</button>
        <button class="ren-btn ren-btn-ghost" type="button">Next</button>
      </div>
    </nav>
  </main>
</body>
</html>


id: example:examples/dialog-workflow.html
type: example
name: dialog-workflow.html
path: examples/dialog-workflow.html
data: {}
body:
<!doctype html>
<html lang="en" data-theme="default">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>RenDS · Dialog workflow example</title>

  <link rel="stylesheet" href="../index.css">
  <link rel="stylesheet" href="../components/index.css">

  <script type="module" src="../components/composites/ren-dialog/ren-dialog.js"></script>
  <script type="module" src="../components/primitives/ren-field/ren-field.js"></script>
  <script type="module" src="../components/patterns/ren-form/ren-form.js"></script>
</head>
<body>
  <main class="ren-center ren-stack-lg" style="padding: var(--space-6)">
    <header class="ren-stack-xs">
      <h1>Project: Atlas</h1>
      <p style="color: var(--color-text-muted)">Manage settings and dangerous operations.</p>
    </header>

    <section class="ren-card ren-stack">
      <header class="ren-card-header">
        <h2 class="ren-card-title">Quick actions</h2>
      </header>
      <div class="ren-card-body ren-cluster">
        <button class="ren-btn ren-btn-secondary" type="button" data-dialog-trigger="invite-dialog">
          Invite collaborator
        </button>
        <button class="ren-btn ren-btn-danger" type="button" data-dialog-trigger="confirm-delete">
          Delete project
        </button>
      </div>
    </section>

    <!--
      Modal dialog with form.
      <ren-dialog> wraps a real <dialog>. The form uses method="dialog"
      so the browser closes the dialog on submit.
    -->
    <ren-dialog id="invite-dialog">
      <dialog class="ren-dialog ren-dialog-md">
        <div class="ren-dialog-header">
          <h2 class="ren-dialog-title">Invite collaborator</h2>
          <button class="ren-dialog-close" data-dialog-close aria-label="Close"></button>
        </div>

        <form method="dialog" class="ren-dialog-body ren-stack" id="invite-form">
          <ren-field>
            <label>Email address</label>
            <input class="ren-input" type="email" name="email" autocomplete="email" required>
          </ren-field>
          <ren-field>
            <label>Role</label>
            <select class="ren-input" name="role">
              <option value="viewer">Viewer</option>
              <option value="editor" selected>Editor</option>
              <option value="admin">Admin</option>
            </select>
          </ren-field>
        </form>

        <div class="ren-dialog-footer">
          <button type="button" class="ren-btn ren-btn-secondary" data-dialog-close>Cancel</button>
          <button class="ren-btn" type="submit" form="invite-form" data-dialog-close="invite">
            Send invite
          </button>
        </div>
      </dialog>
    </ren-dialog>

    <!--
      Alert dialog: critical destructive confirmation.
      `alert` disables backdrop dismissal so it cannot be closed accidentally.
    -->
    <ren-dialog id="confirm-delete" alert>
      <dialog class="ren-dialog ren-dialog-sm ren-alert-dialog">
        <div class="ren-dialog-header">
          <h2 class="ren-dialog-title">Delete project Atlas?</h2>
        </div>
        <div class="ren-dialog-body">
          <p>This permanently removes the project, its members, and all
          historical data. This action cannot be undone.</p>
        </div>
        <div class="ren-dialog-footer">
          <button type="button" class="ren-btn ren-btn-secondary" data-dialog-close>Cancel</button>
          <button type="button" class="ren-btn ren-btn-danger" data-dialog-close="delete">
            Yes, delete
          </button>
        </div>
      </dialog>
    </ren-dialog>
  </main>

  <script type="module">
    document.querySelector('#invite-dialog').addEventListener('ren-close', (event) => {
      if (event.detail.returnValue === 'invite') {
        const form = document.getElementById('invite-form');
        const data = Object.fromEntries(new FormData(form));
        console.log('invite', data);
        // Send invite via API here.
      }
    });

    document.querySelector('#confirm-delete').addEventListener('ren-close', (event) => {
      if (event.detail.returnValue === 'delete') {
        console.log('delete confirmed');
      }
    });
  </script>
</body>
</html>


id: example:examples/settings-form.html
type: example
name: settings-form.html
path: examples/settings-form.html
data: {}
body:
<!doctype html>
<html lang="en" data-theme="default">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>RenDS · Settings form example</title>

  <link rel="stylesheet" href="../index.css">
  <link rel="stylesheet" href="../components/index.css">

  <script type="module" src="../components/primitives/ren-field/ren-field.js"></script>
  <script type="module" src="../components/patterns/ren-form/ren-form.js"></script>
</head>
<body>
  <!--
    Settings: ren-center caps content width, ren-stack-lg spaces sections,
    each section is a ren-card with a ren-form inside.
  -->
  <main class="ren-center ren-stack-lg" style="padding: var(--space-6) var(--space-4)">
    <header class="ren-stack-xs">
      <h1>Settings</h1>
      <p style="color: var(--color-text-muted)">Manage your profile, preferences, and security.</p>
    </header>

    <!-- Profile section -->
    <section class="ren-card">
      <header class="ren-card-header">
        <h2 class="ren-card-title">Profile</h2>
        <p class="ren-card-description">This information will be displayed publicly.</p>
      </header>

      <ren-form data-validate="onTouched">
        <form class="ren-form ren-stack" novalidate>
          <div class="ren-form-row">
            <ren-field data-rules="required">
              <label>First name</label>
              <input class="ren-input" type="text" name="first" autocomplete="given-name" required>
            </ren-field>

            <ren-field data-rules="required">
              <label>Last name</label>
              <input class="ren-input" type="text" name="last" autocomplete="family-name" required>
            </ren-field>
          </div>

          <ren-field data-rules="required|email">
            <label>Email</label>
            <input class="ren-input" type="email" name="email" autocomplete="email" required>
            <span data-description>We'll send a confirmation link to this address.</span>
            <span data-error></span>
          </ren-field>

          <ren-field>
            <label>Bio</label>
            <textarea class="ren-input" name="bio" rows="4" maxlength="240"></textarea>
            <span data-description>Up to 240 characters.</span>
          </ren-field>

          <div class="ren-form-actions">
            <button class="ren-btn ren-btn-secondary" type="button">Reset</button>
            <button class="ren-btn" type="submit">Save profile</button>
          </div>
        </form>
      </ren-form>
    </section>

    <!-- Notifications -->
    <section class="ren-card">
      <header class="ren-card-header">
        <h2 class="ren-card-title">Notifications</h2>
        <p class="ren-card-description">Choose how we contact you.</p>
      </header>

      <form class="ren-card-body ren-stack" novalidate>
        <fieldset class="ren-stack-sm" style="border: 0; padding: 0; margin: 0">
          <legend class="ren-field-label">Email</legend>
          <label class="ren-checkbox">
            <input type="checkbox" name="email_product" checked>
            <span class="ren-checkbox-control"></span>
            <span>Product updates</span>
          </label>
          <label class="ren-checkbox">
            <input type="checkbox" name="email_security" checked>
            <span class="ren-checkbox-control"></span>
            <span>Security alerts</span>
          </label>
          <label class="ren-checkbox">
            <input type="checkbox" name="email_marketing">
            <span class="ren-checkbox-control"></span>
            <span>Marketing emails</span>
          </label>
        </fieldset>

        <div class="ren-form-actions">
          <button class="ren-btn" type="submit">Save notifications</button>
        </div>
      </form>
    </section>

    <!-- Danger zone -->
    <section class="ren-card ren-card-outline" data-status="danger">
      <header class="ren-card-header">
        <h2 class="ren-card-title">Danger zone</h2>
        <p class="ren-card-description">Permanent actions. Double-check before continuing.</p>
      </header>
      <div class="ren-card-body ren-row-spread">
        <p>Delete your account and all associated data.</p>
        <button class="ren-btn ren-btn-danger" type="button" data-dialog-trigger="confirm-delete">
          Delete account
        </button>
      </div>
    </section>
  </main>
</body>
</html>


id: file:components/composites/ren-accordion/ren-accordion.css
type: css
name: ren-accordion.css
path: components/composites/ren-accordion/ren-accordion.css
data: {}
body:
/**
 * RenDS — <ren-accordion> Component
 * ═══════════════════════════════════
 * CSS-first accordion using native <details><summary>
 *
 * Features:
 *   - Smooth height animation using ::details-content
 *   - Chevron rotation on open state
 *   - Multiple variants: default, bordered, flush
 *   - Reduced motion support
 *   - Dark mode support (via semantic tokens)
 *   - Fallback for browsers without ::details-content support
 */

/* ═══════════════════════════════════════════════════════════════════ */
/* ACCORDION CONTAINER */
/* ═══════════════════════════════════════════════════════════════════ */

.ren-accordion {
  display: flex;
  flex-direction: column;
  width: 100%;
  border-top: 1px solid var(--color-border);

  /* Default variant: divider lines between items */
  & > details {
    border-bottom: 1px solid var(--color-border);

    &:last-child {
      border-bottom: none;
    }
  }
}

/* ═══════════════════════════════════════════════════════════════════ */
/* TRIGGER / SUMMARY ELEMENT */
/* ═══════════════════════════════════════════════════════════════════ */

.ren-accordion-trigger,
.ren-accordion > details > summary {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-3) var(--space-4);
  cursor: pointer;
  font-weight: var(--label-lg-weight);
  font-size: var(--label-lg-size);
  line-height: 1.4;
  color: var(--color-text);
  background-color: transparent;
  border: none;
  min-height: var(--size-lg);
  width: 100%;
  text-align: start;
  user-select: none;
  list-style: none;
  transition: var(--transition-tactile);

  /* Remove default browser marker */
  &::marker,
  &::-webkit-details-marker {
    display: none;
    content: '';
  }

  /* Chevron icon via ::after pseudo-element */
  &::after {
    content: '';
    display: inline-block;
    width: 20px;
    height: 20px;
    margin-inline-start: var(--space-3);
    flex-shrink: 0;
    background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>');
    background-repeat: no-repeat;
    background-position: center;
    background-size: contain;
    transition: transform var(--duration-enter) var(--ease-enter);
      border: none;
    transform: rotate(0deg);
  }

  /* Hover state */
  &:hover {
    background-color: var(--color-fill);
  }

  /* Active state */
  &:active {
    background-color: var(--color-fill-active);
  }

  /* Focus ring */
  &:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: -2px;
  }

  /* Disabled state */
  &:disabled,
  &[aria-disabled="true"] {
    opacity: 0.5;
    cursor: not-allowed;

    &:hover {
      background-color: transparent;
    }
  }
}

/* ═══════════════════════════════════════════════════════════════════ */
/* REMOVE NATIVE DISCLOSURE TRIANGLE (WebKit fallback) */
/* ═══════════════════════════════════════════════════════════════════ */

.ren-accordion > details > summary::-webkit-details-marker {
  display: none;
}

.ren-accordion-trigger::-webkit-details-marker {
  display: none;
}

/* ═══════════════════════════════════════════════════════════════════ */
/* OPEN STATE — ROTATE CHEVRON */
/* ═══════════════════════════════════════════════════════════════════ */

.ren-accordion > details[open] > summary::after {
  transform: rotate(180deg);
}

/* Fallback for custom markup */
.ren-accordion-trigger[aria-expanded="true"]::after {
  transform: rotate(180deg);
}

/* Animate the native details content box. The global reset enables
   keyword-size interpolation, so block-size can transition to and from auto. */
@supports selector(details::details-content) {
  .ren-accordion > details::details-content {
    block-size: 0;
    overflow: clip;
    opacity: 0;
    transition:
      block-size var(--duration-exit) var(--ease-exit),
      opacity var(--duration-exit) var(--ease-exit),
      content-visibility var(--duration-exit) var(--ease-exit);
    transition-behavior: allow-discrete;
  }

  .ren-accordion > details[open]::details-content {
    block-size: auto;
    opacity: 1;
    transition-duration: var(--duration-enter);
    transition-timing-function: var(--ease-enter);
  }
}

/* ═══════════════════════════════════════════════════════════════════ */
/* CONTENT ANIMATION — MODERN APPROACH */
/* ═══════════════════════════════════════════════════════════════════ */

.ren-accordion > details {
  /* Modern: use ::details-content for height animation */
  & > .ren-accordion-content {
    padding: 0 var(--space-4) var(--space-4);
    color: var(--color-text-muted);
    line-height: 1.6;
    overflow: hidden;

    /* Browsers with ::details-content support */
    @supports selector(::-webkit-details-content) {
      height: 0;
      overflow: hidden;
      transition:
        height var(--duration-enter) var(--ease-enter),
        opacity var(--duration-enter) var(--ease-enter);
    }

    /* Also try Chrome/V8 syntax (may vary by engine) */
    @supports selector(::details-content) {
      height: 0;
      overflow: hidden;
      transition:
        height var(--duration-enter) var(--ease-enter),
        opacity var(--duration-enter) var(--ease-enter);
    }
  }

  /* Fallback for browsers without ::details-content: use opacity + max-height */
  & > .ren-accordion-content {
    opacity: 0;
    max-height: 0;
    transition:
      opacity var(--duration-enter) var(--ease-enter),
      max-height var(--duration-enter) var(--ease-enter);
  }

  /* When details is open */
  &[open] > .ren-accordion-content {
    opacity: 1;
    max-height: 1000px; /* Reasonable fallback limit */

    /* Modern: animate to auto height with ::details-content */
    @supports selector(::details-content) {
      height: auto;
    }
  }

  /* Entry animation starting point (for ::details-content) */
  @starting-style {
    &[open] > .ren-accordion-content {
      opacity: 0;
      max-height: 0;
    }
  }
}

/* ═══════════════════════════════════════════════════════════════════ */
/* CONTENT ELEMENT */
/* ═══════════════════════════════════════════════════════════════════ */

.ren-accordion-content {
  padding: 0 var(--space-4) var(--space-4);
  color: var(--color-text-muted);
  line-height: 1.6;
}

/* Hide custom content via hidden attribute (for non-details markup) */
.ren-accordion-content[hidden] {
  display: none;
}

/* ═══════════════════════════════════════════════════════════════════ */
/* VARIANT: BORDERED */
/* ═══════════════════════════════════════════════════════════════════ */

.ren-accordion-bordered {
  border-top: none;

  & > details,
  & .ren-accordion-item {
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    margin-bottom: var(--space-2);
    overflow: hidden;

    &:last-child {
      margin-bottom: 0;
    }
  }

  & > details > summary,
  & .ren-accordion-trigger {
    padding: var(--space-4) var(--space-4);
  }

  & > details > .ren-accordion-content,
  & .ren-accordion-content {
    padding: 0 var(--space-4) var(--space-4);
  }
}

/* ═══════════════════════════════════════════════════════════════════ */
/* VARIANT: FLUSH (no borders, minimal spacing) */
/* ═══════════════════════════════════════════════════════════════════ */

.ren-accordion-flush {
  border-top: none;

  & > details,
  & .ren-accordion-item {
    border-bottom: none;
    padding: var(--space-2) 0;

    &:last-child {
      border-bottom: none;
    }
  }

  & > details > summary,
  & .ren-accordion-trigger {
    padding: var(--space-3) 0;
  }

  & > details > .ren-accordion-content,
  & .ren-accordion-content {
    padding: 0 0 var(--space-3);
  }
}

/* ═══════════════════════════════════════════════════════════════════ */
/* REDUCED MOTION */
/* ═══════════════════════════════════════════════════════════════════ */

@media (prefers-reduced-motion: reduce) {
  .ren-accordion-trigger::after,
  .ren-accordion > details > summary::after {
    transition: none;
  }

  .ren-accordion > details > .ren-accordion-content {
    transition: none;
  }

  .ren-accordion > details::details-content {
    transition: none;
  }
}

/* ═══════════════════════════════════════════════════════════════════ */
/* ACCESSIBILITY SUPPORT */
/* ═══════════════════════════════════════════════════════════════════ */

/* Ensure proper contrast for focus states */
.ren-accordion > details > summary:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: -2px;
}

/* Dark mode: tokens will auto-adjust via CSS variables */
@media (prefers-color-scheme: dark) {
  /* Tokens like --color-text, --color-border, etc. should already be defined
     in the design system's color token reset. No additional changes needed. */
}


id: file:components/composites/ren-accordion/ren-accordion.js
type: javascript
name: ren-accordion.js
path: components/composites/ren-accordion/ren-accordion.js
data: {}
body:
/**
 * RenDS — <ren-accordion> (Minimal Enhancement)
 * ════════════════════════════════════════════════
 *
 * Native <details><summary> handles 90% of the work.
 * This JS only adds:
 *   - Exclusive mode (single): close others when one opens
 *     → Uses native <details name="..."> when supported,
 *       with JS fallback for older browsers
 *   - default-value attribute: open items on init
 *   - ren-accordion-change event: custom event dispatch
 *
 * Attributes:
 *   type:          'single' (default) | 'multiple'
 *   collapsible:   boolean (single mode only) — allow all closed
 *   default-value: comma-separated indices to open on init
 *
 * Markup:
 *   <ren-accordion type="single" default-value="0">
 *     <details>
 *       <summary class="ren-accordion-trigger">Item 1</summary>
 *       <div class="ren-accordion-content">Content...</div>
 *     </details>
 *     <details>
 *       <summary class="ren-accordion-trigger">Item 2</summary>
 *       <div class="ren-accordion-content">Content...</div>
 *     </details>
 *   </ren-accordion>
 *
 * Events:
 *   ren-accordion-change: Fired when item open state changes
 *   - detail: { item, isOpen, index }
 */

/**
 * Feature detect: does <details name="..."> support exclusive mode natively?
 * When supported, setting the same `name` on multiple <details> makes them
 * behave like radio buttons — opening one closes the others automatically.
 */
const supportsDetailsName = (() => {
  try {
    const d1 = document.createElement('details');
    const d2 = document.createElement('details');
    d1.name = 'test';
    d2.name = 'test';
    d1.open = true;
    d2.open = true;
    const container = document.createElement('div');
    container.append(d1, d2);
    document.body.append(container);
    // If native exclusive works, opening d2 should close d1
    const works = !d1.open;
    container.remove();
    return works;
  } catch {
    return false;
  }
})();

export class RenAccordion extends HTMLElement {
  static get observedAttributes() {
    return ['type', 'collapsible', 'default-value'];
  }

  constructor() {
    super();
    /** @private */ this._useNativeExclusive = false;
    /** @private */ this._groupName = '';
    this._listenerController = null;
    this._handleToggleBound = (event) => this._handleToggle(event);
  }

  connectedCallback() {
    this._listenerController?.abort();
    this._listenerController = new AbortController();
    this.classList.add('ren-accordion');

    // Determine mode (exclusive or multiple)
    this._exclusive = this.getAttribute('type') !== 'multiple';
    this._collapsible = this.hasAttribute('collapsible');

    // Generate a unique group name for native <details name>
    this._groupName = `ren-accordion-${this.id || Math.random().toString(36).slice(2, 8)}`;

    // Use native exclusive mode when browser supports it
    this._useNativeExclusive = supportsDetailsName && this._exclusive;
    this._applyNativeExclusive();

    // Listen for toggle events on all child details (event delegation)
    this.addEventListener('toggle', this._handleToggleBound, {
      capture: true,
      signal: this._listenerController.signal,
    });

    // Apply default open items
    const defaultValue = this.getAttribute('default-value');
    if (defaultValue) {
      this._setDefaultOpen(defaultValue);
    }
  }

  disconnectedCallback() {
    this._listenerController?.abort();
    this._listenerController = null;
  }

  /**
   * Apply or remove the native `name` attribute on child <details>.
   * When supported, this lets the browser handle exclusive mode natively.
   * @private
   */
  _applyNativeExclusive() {
    this.items.forEach((detail) => {
      // Closed accordion items must keep their summary visible. The global
      // data-state helper hides closed surfaces unless they explicitly opt out.
      detail.setAttribute('data-keep', '');

      if (this._useNativeExclusive) {
        detail.setAttribute('name', this._groupName);
      } else {
        // Remove name if switching to multiple mode or no native support
        if (detail.getAttribute('name') === this._groupName) {
          detail.removeAttribute('name');
        }
      }
    });
  }

  /**
   * Handle toggle event on child <details> elements
   * @private
   */
  _handleToggle(event) {
    const details = event.target;

    // Only handle <details> that are direct children or nested in .ren-accordion-item
    if (!this._isValidDetail(details)) {
      return;
    }

    // Set data-state for CSS styling
    details.setAttribute('data-state', details.open ? 'open' : 'closed');

    // JS fallback for exclusive mode (only when native isn't handling it)
    if (this._exclusive && !this._useNativeExclusive && details.open) {
      this._closeOtherDetails(details);
    }

    // Sync data-state on siblings closed by native <details name>
    if (this._useNativeExclusive && details.open) {
      this.items.forEach((d) => {
        if (d !== details) {
          d.setAttribute('data-state', d.open ? 'open' : 'closed');
        }
      });
    }

    // Dispatch custom event
    const index = this.items.indexOf(details);
    this.dispatchEvent(
      new CustomEvent('ren-accordion-change', {
        detail: {
          item: details,
          isOpen: details.open,
          index,
        },
        bubbles: true,
      })
    );
  }

  /**
   * Check if a details element is a valid child
   * @private
   */
  _isValidDetail(details) {
    if (details.tagName !== 'DETAILS') {
      return false;
    }

    // Direct child of accordion
    if (details.parentElement === this) {
      return true;
    }

    // Child of .ren-accordion-item within accordion
    const parent = details.parentElement;
    if (
      parent &&
      parent.classList &&
      parent.classList.contains('ren-accordion-item') &&
      parent.parentElement === this
    ) {
      return true;
    }

    return false;
  }

  /**
   * Close all other details except the specified one
   * @private
   */
  _closeOtherDetails(openDetails) {
    this.items.forEach((detail) => {
      if (detail !== openDetails && detail.open) {
        detail.open = false;
        detail.setAttribute('data-state', 'closed');
      }
    });
  }

  /**
   * Set which items should be open by default
   * @private
   */
  _setDefaultOpen(value) {
    const indices = value
      .split(',')
      .map((s) => parseInt(s.trim(), 10))
      .filter((i) => !isNaN(i));

    this.items.forEach((detail, index) => {
      detail.open = indices.includes(index);
      detail.setAttribute('data-state', detail.open ? 'open' : 'closed');
    });
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (!this.isConnected) return;

    if (name === 'type') {
      // Update exclusive mode
      this._exclusive = newValue !== 'multiple';
      this._useNativeExclusive = supportsDetailsName && this._exclusive;
      this._applyNativeExclusive();
    } else if (name === 'collapsible') {
      // Collapsible flag updated
      this._collapsible = this.hasAttribute('collapsible');
    } else if (name === 'default-value') {
      // Re-apply defaults
      if (newValue) {
        this._setDefaultOpen(newValue);
      }
    }
  }

  /* ═══════════════════════════════════════════ */
  /* PUBLIC API */
  /* ═══════════════════════════════════════════ */

  /**
   * Get all details elements
   */
  get items() {
    const items = [];

    // Direct <details> children
    this.querySelectorAll(':scope > details').forEach((d) => items.push(d));

    // <details> nested in .ren-accordion-item children
    this.querySelectorAll(':scope > .ren-accordion-item > details').forEach(
      (d) => items.push(d)
    );

    return items;
  }

  /**
   * Open a specific item by index
   */
  openItem(index) {
    const item = this.items[index];
    if (!item) return;

    // Exclusive mode: close others
    if (this._exclusive) {
      this._closeOtherDetails(item);
    }

    item.open = true;
  }

  /**
   * Close a specific item by index
   */
  closeItem(index) {
    const item = this.items[index];
    if (!item) return;

    // In exclusive mode without collapsible: prevent closing if only open item
    if (
      this._exclusive &&
      !this._collapsible &&
      this.items.filter((d) => d.open).length === 1 &&
      item.open
    ) {
      return; // Keep at least one open
    }

    item.open = false;
  }

  /**
   * Toggle a specific item by index
   */
  toggleItem(index) {
    const item = this.items[index];
    if (!item) return;

    if (item.open) {
      this.closeItem(index);
    } else {
      this.openItem(index);
    }
  }

  /**
   * Check if item is open
   */
  isOpen(index) {
    return this.items[index]?.open ?? false;
  }

  /**
   * Get all currently open indices
   */
  getOpenItems() {
    return this.items
      .map((item, index) => (item.open ? index : -1))
      .filter((i) => i !== -1);
  }
}

// Register the component
if (!customElements.get('ren-accordion')) {
  customElements.define('ren-accordion', RenAccordion);
}


id: file:components/composites/ren-alert-dialog/ren-alert-dialog.css
type: css
name: ren-alert-dialog.css
path: components/composites/ren-alert-dialog/ren-alert-dialog.css
data: {}
body:
/* ============================================
   RenDS — Alert Dialog Component
   ============================================
   Confirmation dialog that blocks interaction.
   Extends native <dialog> like ren-dialog but:
   - No dismiss on outside click (::backdrop blocks)
   - No dismiss on Escape (optional)
   - Requires explicit action (confirm/cancel)

   Minimal JS: just showModal() and close().

   Usage:
     <dialog class="ren-alert-dialog" id="confirm">
       <div class="ren-alert-dialog-icon">⚠</div>
       <h2 class="ren-alert-dialog-title">Delete item?</h2>
       <p class="ren-alert-dialog-description">
         This action cannot be undone.
       </p>
       <div class="ren-alert-dialog-actions">
         <button class="ren-btn-secondary" onclick="this.closest('dialog').close('cancel')">
           Cancel
         </button>
         <button class="ren-btn-danger" onclick="this.closest('dialog').close('confirm')">
           Delete
         </button>
       </div>
     </dialog>

   Open: document.getElementById('confirm').showModal()
   Result: dialog.addEventListener('close', () => dialog.returnValue)
   ============================================ */

.ren-alert-dialog {
  container-type: inline-size;
  container-name: ren-alert-dialog;

  /* Structure */
  margin: auto;
  padding: var(--space-6);
  border: none;

  /* Sizing */
  max-width: 24rem;
  width: 90vw;

  /* Appearance */
  background: var(--color-surface);
  color: var(--color-text);
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-xl);
  text-align: center;

  /* Animation — semantic tokens keep this in sync with ren-dialog
     and respect prefers-reduced-motion via tokens/semantic/motion.css. */
  opacity: 1;
  transform: scale(1);
  transition:
    opacity   var(--duration-enter) var(--ease-enter),
    transform var(--duration-enter) var(--ease-enter),
    overlay   var(--duration-enter) var(--ease-enter) allow-discrete,
    display   var(--duration-enter) var(--ease-enter) allow-discrete;
}

/* Entry animation */
@starting-style {
  .ren-alert-dialog[open] {
    opacity: 0;
    transform: scale(0.95);
  }
}

/* Exit */
.ren-alert-dialog:not([open]) {
  opacity: 0;
  transform: scale(0.95);
}

/* Backdrop — shared overlay transition keeps all scrims in sync. */
.ren-alert-dialog::backdrop {
  background-color: var(--color-overlay);
  transition: var(--transition-overlay);
}

@starting-style {
  .ren-alert-dialog[open]::backdrop {
    opacity: 0;
  }
}

/* ─── Icon (optional) ─── */
.ren-alert-dialog-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--icon-xl);
  height: var(--icon-xl);
  margin: 0 auto var(--space-3);
  font-size: var(--text-xl);
  background-color: var(--color-fill);
  border-radius: var(--radius-full);
}

/* Danger icon */
.ren-alert-dialog-icon-danger {
  background-color: var(--color-danger-subtle);
  color: var(--color-danger);
}

/* Warning icon */
.ren-alert-dialog-icon-warning {
  background-color: var(--color-warning-subtle);
  color: var(--color-warning);
}

/* ─── Title ─── */
.ren-alert-dialog-title {
  font-size: var(--title-sm-size, var(--text-lg));
  font-weight: var(--weight-semibold);
  color: var(--color-text);
  margin-bottom: var(--space-2);
}

/* ─── Description ─── */
.ren-alert-dialog-description {
  font-size: var(--body-size);
  color: var(--color-text-muted);
  line-height: var(--leading-normal);
  margin-bottom: var(--space-5);
}

/* ─── Actions ─── */
.ren-alert-dialog-actions {
  display: flex;
  gap: var(--space-2);
  justify-content: center;
}

/* Stack actions when dialog is narrow */
@container ren-alert-dialog (max-width: 24rem) {
  .ren-alert-dialog-actions {
    flex-direction: column-reverse;
  }

  .ren-alert-dialog-actions > * {
    width: 100%;
  }
}

/* ─── Reduced motion ─── */
@media (prefers-reduced-motion: reduce) {
  .ren-alert-dialog {
    transition: none;
  }
  .ren-alert-dialog::backdrop {
    transition: none;
  }
}


id: file:components/composites/ren-calendar/ren-calendar.css
type: css
name: ren-calendar.css
path: components/composites/ren-calendar/ren-calendar.css
data: {}
body:
/* ═══ REN CALENDAR WEB COMPONENT ═══
   A fully accessible, keyboard-navigable calendar component
   with support for single, range, and multiple date selection.
   Uses ARIA grid pattern with roving tabindex for keyboard navigation.
   ══════════════════════════════════════════════════════════════════ */

/* ═══ BASE CONTAINER ═══ */
.ren-calendar {
  container-type: inline-size;
  container-name: ren-calendar;
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
  padding: var(--space-4);
  font-family: inherit;
}

/* ═══ HEADER WITH NAVIGATION ═══ */
.ren-calendar-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--space-4);
  gap: var(--space-2);
}

/* ═══ MONTH/YEAR TITLE ═══ */
.ren-calendar-title {
  font-size: var(--body-size);
  font-weight: var(--label-weight);
  color: var(--color-text);
  flex: 1;
  text-align: center;
  min-width: 150px;
  white-space: nowrap;
}

/* ═══ NAVIGATION BUTTONS ═══ */
.ren-calendar-nav {
  display: flex;
  gap: var(--space-1);
}

.ren-calendar-nav button {
  width: var(--touch-min);
  height: var(--touch-min);
  min-width: var(--touch-min);
  min-height: var(--touch-min);
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  border-radius: var(--radius-md);
  color: var(--color-text);
  cursor: pointer;
  transition: var(--transition-tactile);
  position: relative;

  &:hover {
    background: var(--color-fill);
  }

  &:active {
    background: var(--color-fill-active);
  }

  &:disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }

  &:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 2px;
  }

  /* ═══ CHEVRON ICONS ═══ */
  &.ren-calendar-prev::before {
    content: '‹';
    font-size: 1.5em;
    font-weight: bold;
  }

  &.ren-calendar-next::before {
    content: '›';
    font-size: 1.5em;
    font-weight: bold;
  }
}

/* ═══ WEEKDAY HEADERS ═══ */
.ren-calendar-weekdays {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: var(--space-1);
  margin-bottom: var(--space-2);
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  color: var(--color-text-muted);
  text-align: center;
}

.ren-calendar-weekday {
  padding: var(--space-2) var(--space-1);
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

/* ═══ CALENDAR GRID ═══ */
.ren-calendar-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: var(--space-1);
}

/* ═══ INDIVIDUAL DAY BUTTON ═══ */
.ren-calendar-day {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: var(--touch-min);
  min-height: var(--touch-min);
  width: 100%;
  aspect-ratio: 1;
  padding: var(--space-0-5);
  background: transparent;
  border: none;
  border-radius: var(--radius-md);
  color: var(--color-text);
  font-size: var(--body-size);
  cursor: pointer;
  transition: var(--transition-tactile);
  position: relative;

  /* ═══ DEFAULT STATE ═══ */
  &:not([disabled]):hover {
    background: var(--color-fill);
  }

  &:not([disabled]):active {
    background: var(--color-fill-active);
  }

  &:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 2px;
  }

  /* ═══ SELECTED DATE ═══ */
  &[aria-selected="true"] {
    background: var(--color-accent);
    color: var(--color-on-accent);
    font-weight: 600;
  }

  /* ═══ TODAY INDICATOR ═══ */
  &[data-today] {
    background: var(--color-accent-subtle);
    color: var(--color-accent);
    font-weight: 600;
    box-shadow: inset 0 0 0 2px var(--color-accent);
  }

  &[data-today][aria-selected="true"] {
    background: var(--color-accent);
    color: var(--color-on-accent);
    box-shadow: none;
  }

  /* ═══ OUTSIDE MONTH DATES ═══ */
  &[data-outside] {
    color: var(--color-text-faint);
    pointer-events: none;
  }

  /* ═══ DISABLED DATES ═══ */
  &[disabled] {
    color: var(--color-text-muted);
    cursor: not-allowed;
    opacity: 0.5;
  }

  /* ═══ RANGE START ═══ */
  &.ren-calendar-day-range-start {
    background: var(--color-accent);
    color: var(--color-on-accent);
    border-radius: var(--radius-md);

    &::after {
      content: '';
      position: absolute;
      right: -4px;
      top: 0;
      bottom: 0;
      width: 50%;
      background: var(--color-accent-subtle);
      z-index: -1;
    }
  }

  /* ═══ RANGE END ═══ */
  &.ren-calendar-day-range-end {
    background: var(--color-accent);
    color: var(--color-on-accent);
    border-radius: var(--radius-md);

    &::before {
      content: '';
      position: absolute;
      left: -4px;
      top: 0;
      bottom: 0;
      width: 50%;
      background: var(--color-accent-subtle);
      z-index: -1;
    }
  }

  /* ═══ IN RANGE ═══ */
  &.ren-calendar-day-in-range {
    background: var(--color-accent-subtle);
    color: var(--color-text);
    border-radius: 0;
  }
}

/* ═══ ANIMATION FOR MONTH TRANSITIONS ═══ */
@keyframes ren-calendar-fade-in {
  from {
    opacity: 0;
    transform: translateY(-4px);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.ren-calendar-grid {
  animation: ren-calendar-fade-in var(--duration-enter) var(--ease-enter);
}

/* ═══ REDUCED MOTION SUPPORT ═══ */
@media (prefers-reduced-motion: reduce) {
  .ren-calendar-grid {
    animation: none;
  }

  .ren-calendar-nav button,
  .ren-calendar-day {
    transition: none;
  }
}

/* ═══ SIZE VARIANT: SMALL ═══ */
.ren-calendar-sm {
  padding: var(--space-3);

  .ren-calendar-header {
    margin-bottom: var(--space-3);
  }

  .ren-calendar-title {
    font-size: var(--label-size);
  }

  .ren-calendar-nav button {
    width: 36px;
    height: 36px;
    min-width: 36px;
    min-height: 36px;
  }

  .ren-calendar-weekday {
    font-size: var(--caption-size);
  }

  .ren-calendar-day {
    font-size: var(--label-size);
    min-width: 36px;
    min-height: 36px;
  }
}

/* ═══ SIZE VARIANT: LARGE ═══ */
.ren-calendar-lg {
  padding: var(--space-6);

  .ren-calendar-header {
    margin-bottom: var(--space-6);
    gap: var(--space-4);
  }

  .ren-calendar-title {
    font-size: 1.25rem;
  }

  .ren-calendar-nav button {
    width: 52px;
    height: 52px;
    min-width: 52px;
    min-height: 52px;
    font-size: 1.25rem;
  }

  .ren-calendar-weekdays {
    margin-bottom: var(--space-4);
  }

  .ren-calendar-grid {
    gap: var(--space-2);
  }

  .ren-calendar-day {
    font-size: 1.125rem;
    min-width: 52px;
    min-height: 52px;
  }
}

/* ═══ DARK MODE SUPPORT ═══ */
@media (prefers-color-scheme: dark) {
  .ren-calendar {
    /* ═══ USES CSS VARIABLES - AUTO ADAPTS ═══ */
  }
}

/* ═══ COMPACT LAYOUT (Container Query) ═══ */
@container ren-calendar (max-width: 320px) {
  .ren-calendar {
    padding: var(--space-3);
  }

  .ren-calendar-grid {
    gap: var(--space-0-5);
  }

  .ren-calendar-day {
    min-width: 32px;
    min-height: 32px;
    font-size: var(--label-size);
  }

  .ren-calendar-nav button {
    width: 40px;
    height: 40px;
    min-width: 40px;
    min-height: 40px;
  }
}
/* Appearance API bridge */
.ren-calendar {
  inline-size: var(--ren-calendar-width, 280px);
  border-radius: var(--ren-calendar-radius, var(--radius-lg));
  background: var(--ren-calendar-bg, var(--color-surface));
  border-color: var(--ren-calendar-border, var(--color-border));
}
.ren-calendar [data-day], .ren-calendar-day { inline-size: var(--ren-calendar-day-size, var(--size-md)); block-size: var(--ren-calendar-day-size, var(--size-md)); }
.ren-calendar [data-today] { background: var(--ren-calendar-today-bg, var(--color-accent-subtle)); }
.ren-calendar [aria-selected="true"], .ren-calendar [data-selected] { background: var(--ren-calendar-selected-bg, var(--color-accent)); color: var(--ren-calendar-selected-color, var(--color-on-accent)); }
.ren-calendar [data-range], .ren-calendar-range { background: var(--ren-calendar-range-bg, var(--color-accent-subtle)); }

@media (any-pointer: coarse), (pointer: coarse) {
  .ren-calendar .ren-calendar-nav button,
  .ren-calendar .ren-calendar-day {
    inline-size: calc(var(--touch-min) + var(--stroke-1));
    block-size: calc(var(--touch-min) + var(--stroke-1));
    min-inline-size: calc(var(--touch-min) + var(--stroke-1));
    min-block-size: calc(var(--touch-min) + var(--stroke-1));
  }
}


id: file:components/composites/ren-calendar/ren-calendar.js
type: javascript
name: ren-calendar.js
path: components/composites/ren-calendar/ren-calendar.js
data: {}
body:
import { formatLocalDate, parseLocalDate } from '../../../utils/local-date.js';

/* ═══ REN CALENDAR WEB COMPONENT ═══
   A fully accessible, keyboard-navigable calendar for date selection.

   Features:
   - Multiple selection modes: single, range, multiple
   - Full keyboard navigation with ARIA grid pattern
   - Locale-aware date formatting
   - Customizable min/max dates
   - Month navigation with prev/next buttons
   - Today indicator and outside-month date styling

   Usage:
   <ren-calendar value="2026-03-31" locale="en-US" mode="single"></ren-calendar>
   ══════════════════════════════════════════════════════════════════ */

export class RenCalendar extends HTMLElement {
  constructor() {
    super();

    /* ═══ STATE ═══ */
    this.currentDate = new Date();
    this.selectedDate = null;
    this.selectedDates = new Set();
    this.rangeStart = null;
    this.rangeEnd = null;
    this.focusedDate = null;

    /* ═══ CONFIGURATION ═══ */
    this.mode = 'single'; // single, range, multiple
    this.locale = 'en-US';
    this.firstDay = 0; // 0 = Sunday, 1 = Monday
    this.minDate = null;
    this.maxDate = null;

    /* ═══ BIND METHODS ═══ */
    this.handleDayClick = this.handleDayClick.bind(this);
    this.handleDayKeyDown = this.handleDayKeyDown.bind(this);
    this.handlePrevMonth = this.handlePrevMonth.bind(this);
    this.handleNextMonth = this.handleNextMonth.bind(this);
  }

  connectedCallback() {
    /* ═══ READ ATTRIBUTES ═══ */
    this.mode = this.getAttribute('mode') || 'single';
    this.locale = this.getAttribute('locale') || 'en-US';
    const firstDayAttr = this.getAttribute('first-day');
    if (firstDayAttr !== null) this.firstDay = parseInt(firstDayAttr, 10);

    /* ═══ PARSE DATE ATTRIBUTES ═══ */
    const valueAttr = this.getAttribute('value');
    if (valueAttr) {
      const parsed = this.parseDate(valueAttr);
      if (!isNaN(parsed)) {
        this.selectedDate = parsed;
        this.currentDate = new Date(parsed);
      }
    }

    const minAttr = this.getAttribute('min');
    if (minAttr) this.minDate = this.parseDate(minAttr);

    const maxAttr = this.getAttribute('max');
    if (maxAttr) this.maxDate = this.parseDate(maxAttr);

    /* ═══ RENDER CALENDAR ═══ */
    this.render();
  }

  /* ═══ RENDER CALENDAR ═══ */
  render() {
    this.innerHTML = '';
    this.classList.add('ren-calendar');

    /* ═══ BUILD HEADER ═══ */
    const header = document.createElement('div');
    header.className = 'ren-calendar-header';

    const title = document.createElement('div');
    title.className = 'ren-calendar-title';
    title.textContent = this.formatMonthYear();
    title.setAttribute('aria-live', 'polite');

    const navDiv = document.createElement('div');
    navDiv.className = 'ren-calendar-nav';

    const prevBtn = document.createElement('button');
    prevBtn.className = 'ren-calendar-prev';
    prevBtn.setAttribute('aria-label', `Previous month (${this.formatPrevMonth()})`);
    prevBtn.addEventListener('click', this.handlePrevMonth);

    const nextBtn = document.createElement('button');
    nextBtn.className = 'ren-calendar-next';
    nextBtn.setAttribute('aria-label', `Next month (${this.formatNextMonth()})`);
    nextBtn.addEventListener('click', this.handleNextMonth);

    navDiv.appendChild(prevBtn);
    navDiv.appendChild(nextBtn);

    header.appendChild(title);
    header.appendChild(navDiv);
    this.appendChild(header);

    /* ═══ BUILD WEEKDAY HEADERS ═══ */
    const weekdaysDiv = document.createElement('div');
    weekdaysDiv.className = 'ren-calendar-weekdays';

    const weekdayNames = this.getWeekdayNames();
    weekdayNames.forEach((name) => {
      const dayLabel = document.createElement('div');
      dayLabel.className = 'ren-calendar-weekday';
      dayLabel.textContent = name;
      dayLabel.setAttribute('aria-label', name);
      weekdaysDiv.appendChild(dayLabel);
    });

    this.appendChild(weekdaysDiv);

    /* ═══ BUILD CALENDAR GRID ═══ */
    const grid = document.createElement('div');
    grid.className = 'ren-calendar-grid';
    grid.setAttribute('role', 'grid');

    const dates = this.generateCalendarDates();
    dates.forEach((date, index) => {
      const dayBtn = document.createElement('button');
      dayBtn.className = 'ren-calendar-day';
      dayBtn.textContent = date.date.getDate();
      dayBtn.setAttribute('type', 'button');
      dayBtn.setAttribute('role', 'gridcell');
      dayBtn.setAttribute('data-date', this.dateToString(date.date));

      /* ═══ HANDLE OUTSIDE MONTH DATES ═══ */
      if (date.outside) {
        dayBtn.setAttribute('data-outside', '');
        dayBtn.setAttribute('aria-disabled', 'true');
        dayBtn.disabled = true;
      }

      /* ═══ CHECK IF DATE IS DISABLED ═══ */
      if (this.isDateDisabled(date.date)) {
        dayBtn.disabled = true;
        dayBtn.setAttribute('aria-disabled', 'true');
      }

      /* ═══ CHECK IF TODAY ═══ */
      if (this.isToday(date.date)) {
        dayBtn.setAttribute('data-today', '');
      }

      /* ═══ CHECK IF SELECTED (SINGLE MODE) ═══ */
      if (this.mode === 'single' && this.selectedDate && this.isSameDay(date.date, this.selectedDate)) {
        dayBtn.setAttribute('aria-selected', 'true');
        this.focusedDate = date.date;
      }

      /* ═══ CHECK IF SELECTED (MULTIPLE MODE) ═══ */
      if (this.mode === 'multiple' && this.selectedDates.has(this.dateToString(date.date))) {
        dayBtn.setAttribute('aria-selected', 'true');
      }

      /* ═══ CHECK RANGE SELECTION ═══ */
      if (this.mode === 'range' && this.rangeStart && this.rangeEnd) {
        const dateStr = this.dateToString(date.date);
        const rangeStart = this.dateToString(this.rangeStart);
        const rangeEnd = this.dateToString(this.rangeEnd);

        if (dateStr === rangeStart) {
          dayBtn.classList.add('ren-calendar-day-range-start');
          dayBtn.setAttribute('aria-selected', 'true');
        } else if (dateStr === rangeEnd) {
          dayBtn.classList.add('ren-calendar-day-range-end');
          dayBtn.setAttribute('aria-selected', 'true');
        } else if (dateStr > rangeStart && dateStr < rangeEnd) {
          dayBtn.classList.add('ren-calendar-day-in-range');
        }
      }

      /* ═══ EVENT LISTENERS ═══ */
      dayBtn.addEventListener('click', (e) => this.handleDayClick(e, date.date));
      dayBtn.addEventListener('keydown', (e) => this.handleDayKeyDown(e, date.date, index, dates.length));

      grid.appendChild(dayBtn);
    });

    this.appendChild(grid);

    /* ═══ SET TABINDEX FOR FOCUS MANAGEMENT ═══ */
    this.updateTabIndex();
  }

  /* ═══ UPDATE TABINDEX FOR ROVING TABINDEX PATTERN ═══ */
  updateTabIndex() {
    const buttons = Array.from(this.querySelectorAll('.ren-calendar-day'));
    const enabledButtons = buttons.filter((btn) => !btn.disabled);
    let target = null;

    if (this.focusedDate) {
      const focusedDateString = this.dateToString(this.focusedDate);
      target = enabledButtons.find((btn) => btn.dataset.date === focusedDateString);
    }

    if (!target && this.selectedDate) {
      const selectedDateString = this.dateToString(this.selectedDate);
      target = enabledButtons.find((btn) => btn.dataset.date === selectedDateString);
    }

    if (!target) {
      target = enabledButtons.find((btn) => btn.hasAttribute('data-today'));
    }

    target ??= enabledButtons[0] ?? null;

    buttons.forEach((btn) => {
      btn.setAttribute('tabindex', btn === target ? '0' : '-1');
    });
  }

  /* ═══ GENERATE CALENDAR DATES ═══ */
  generateCalendarDates() {
    const year = this.currentDate.getFullYear();
    const month = this.currentDate.getMonth();

    /* ═══ FIRST DAY OF MONTH AND WEEKDAY ═══ */
    const firstDay = new Date(year, month, 1);
    const lastDay = new Date(year, month + 1, 0);
    const daysInMonth = lastDay.getDate();

    /* ═══ STARTING WEEKDAY ═══ */
    let startingDayOfWeek = firstDay.getDay() - this.firstDay;
    if (startingDayOfWeek < 0) startingDayOfWeek += 7;

    const dates = [];

    /* ═══ PREVIOUS MONTH DATES ═══ */
    const prevMonthLastDay = new Date(year, month, 0).getDate();
    for (let i = startingDayOfWeek - 1; i >= 0; i--) {
      const prevDate = new Date(year, month - 1, prevMonthLastDay - i);
      dates.push({ date: prevDate, outside: true });
    }

    /* ═══ CURRENT MONTH DATES ═══ */
    for (let day = 1; day <= daysInMonth; day++) {
      dates.push({ date: new Date(year, month, day), outside: false });
    }

    /* ═══ NEXT MONTH DATES ═══ */
    const remainingCells = 42 - dates.length; // 6 rows × 7 cols
    for (let day = 1; day <= remainingCells; day++) {
      dates.push({ date: new Date(year, month + 1, day), outside: true });
    }

    return dates;
  }

  /* ═══ GET WEEKDAY NAMES ═══ */
  getWeekdayNames() {
    const formatter = new Intl.DateTimeFormat(this.locale, { weekday: 'short' });
    const weekdays = [];

    for (let i = 0; i < 7; i++) {
      const dayIndex = (i + this.firstDay) % 7;
      const date = new Date(2024, 0, dayIndex + 7); // Sunday is Jan 7, 2024
      weekdays.push(formatter.format(date).toUpperCase().substring(0, 2));
    }

    return weekdays;
  }

  /* ═══ FORMAT MONTH AND YEAR ═══ */
  formatMonthYear() {
    const formatter = new Intl.DateTimeFormat(this.locale, { month: 'long', year: 'numeric' });
    return formatter.format(this.currentDate);
  }

  /* ═══ FORMAT PREVIOUS MONTH ═══ */
  formatPrevMonth() {
    const prevMonth = new Date(this.currentDate.getFullYear(), this.currentDate.getMonth() - 1);
    const formatter = new Intl.DateTimeFormat(this.locale, { month: 'long', year: 'numeric' });
    return formatter.format(prevMonth);
  }

  /* ═══ FORMAT NEXT MONTH ═══ */
  formatNextMonth() {
    const nextMonth = new Date(this.currentDate.getFullYear(), this.currentDate.getMonth() + 1);
    const formatter = new Intl.DateTimeFormat(this.locale, { month: 'long', year: 'numeric' });
    return formatter.format(nextMonth);
  }

  /* ═══ HANDLE DAY CLICK ═══ */
  handleDayClick(event, date) {
    if (event.target.disabled || event.target.hasAttribute('data-outside')) {
      return;
    }

    this.selectDate(date);
  }

  /* ═══ SELECT DATE ═══ */
  selectDate(date) {
    if (this.mode === 'single') {
      this.selectedDate = date;
      this.focusedDate = date;
      this.dispatchDateSelectEvent();
    } else if (this.mode === 'range') {
      if (!this.rangeStart) {
        this.rangeStart = date;
      } else if (!this.rangeEnd) {
        if (date < this.rangeStart) {
          this.rangeEnd = this.rangeStart;
          this.rangeStart = date;
        } else {
          this.rangeEnd = date;
        }
      } else {
        this.rangeStart = date;
        this.rangeEnd = null;
      }
      this.dispatchDateSelectEvent();
    } else if (this.mode === 'multiple') {
      const dateStr = this.dateToString(date);
      if (this.selectedDates.has(dateStr)) {
        this.selectedDates.delete(dateStr);
      } else {
        this.selectedDates.add(dateStr);
      }
      this.dispatchDateSelectEvent();
    }

    this.render();
  }

  /* ═══ HANDLE DAY KEYBOARD NAVIGATION ═══ */
  handleDayKeyDown(event, date, index, totalDates) {
    const key = event.key;

    if (key === 'Enter' || key === ' ') {
      event.preventDefault();
      this.selectDate(date);
      return;
    }

    if (key === 'ArrowUp') {
      event.preventDefault();
      const newIndex = Math.max(0, index - 7);
      this.focusDateAtIndex(newIndex);
      return;
    }

    if (key === 'ArrowDown') {
      event.preventDefault();
      const newIndex = Math.min(totalDates - 1, index + 7);
      this.focusDateAtIndex(newIndex);
      return;
    }

    if (key === 'ArrowLeft') {
      event.preventDefault();
      const newIndex = Math.max(0, index - 1);
      this.focusDateAtIndex(newIndex);
      return;
    }

    if (key === 'ArrowRight') {
      event.preventDefault();
      const newIndex = Math.min(totalDates - 1, index + 1);
      this.focusDateAtIndex(newIndex);
      return;
    }

    if (key === 'Home') {
      event.preventDefault();
      const weekStart = Math.floor(index / 7) * 7;
      this.focusDateAtIndex(weekStart);
      return;
    }

    if (key === 'End') {
      event.preventDefault();
      const weekEnd = Math.ceil((index + 1) / 7) * 7 - 1;
      this.focusDateAtIndex(Math.min(totalDates - 1, weekEnd));
      return;
    }

    if (key === 'PageUp') {
      event.preventDefault();
      this.handlePrevMonth();
      return;
    }

    if (key === 'PageDown') {
      event.preventDefault();
      this.handleNextMonth();
      return;
    }
  }

  /* ═══ FOCUS DATE AT INDEX ═══ */
  focusDateAtIndex(index) {
    const buttons = this.querySelectorAll('.ren-calendar-day');
    if (buttons[index] && !buttons[index].disabled) {
      this.focusedDate = parseLocalDate(buttons[index].dataset.date);
      this.updateTabIndex();
      buttons[index].focus();
    }
  }

  /* ═══ HANDLE PREVIOUS MONTH ═══ */
  handlePrevMonth() {
    this.currentDate = new Date(this.currentDate.getFullYear(), this.currentDate.getMonth() - 1);
    this.render();
  }

  /* ═══ HANDLE NEXT MONTH ═══ */
  handleNextMonth() {
    this.currentDate = new Date(this.currentDate.getFullYear(), this.currentDate.getMonth() + 1);
    this.render();
  }

  /* ═══ GO TO MONTH ═══ */
  goToMonth(year, month) {
    this.currentDate = new Date(year, month);
    this.render();
  }

  /* ═══ SET RANGE ═══ */
  setRange(start, end) {
    if (this.mode !== 'range') {
      console.warn('RenCalendar: setRange() only works in range mode');
      return;
    }

    this.rangeStart = this.parseDate(start);
    this.rangeEnd = this.parseDate(end);
    this.render();
  }

  /* ═══ DISPATCH CUSTOM EVENT ═══ */
  dispatchDateSelectEvent() {
    let eventData = { date: this.selectedDate };

    if (this.mode === 'range') {
      eventData = { range: { start: this.rangeStart, end: this.rangeEnd } };
    } else if (this.mode === 'multiple') {
      eventData = { dates: Array.from(this.selectedDates).map((d) => parseLocalDate(d)) };
    }

    this.dispatchEvent(
      new CustomEvent('ren-date-select', {
        detail: eventData,
        bubbles: true,
        composed: true,
      })
    );
  }

  /* ═══ UTILITY METHODS ═══ */

  /* ═══ CHECK IF DATE IS TODAY ═══ */
  isToday(date) {
    const today = new Date();
    return this.isSameDay(date, today);
  }

  /* ═══ CHECK IF TWO DATES ARE THE SAME DAY ═══ */
  isSameDay(date1, date2) {
    return (
      date1.getFullYear() === date2.getFullYear() &&
      date1.getMonth() === date2.getMonth() &&
      date1.getDate() === date2.getDate()
    );
  }

  /* ═══ CHECK IF DATE IS DISABLED ═══ */
  isDateDisabled(date) {
    if (this.minDate && date < this.minDate) return true;
    if (this.maxDate && date > this.maxDate) return true;
    return false;
  }

  /* ═══ CONVERT DATE TO ISO STRING ═══ */
  dateToString(date) {
    return formatLocalDate(date);
  }

  parseDate(value) {
    return parseLocalDate(value);
  }

  /* ═══ PUBLIC METHODS ═══ */

  /* ═══ GET VALUE ═══ */
  getValue() {
    if (this.mode === 'single') {
      return this.selectedDate ? this.dateToString(this.selectedDate) : null;
    } else if (this.mode === 'range') {
      return this.rangeStart && this.rangeEnd
        ? { start: this.dateToString(this.rangeStart), end: this.dateToString(this.rangeEnd) }
        : null;
    } else if (this.mode === 'multiple') {
      return Array.from(this.selectedDates);
    }
  }

  /* ═══ SET VALUE ═══ */
  setValue(value) {
    if (this.mode === 'single' && typeof value === 'string') {
    this.selectedDate = this.parseDate(value);
      this.render();
    } else if (this.mode === 'range' && value && typeof value === 'object') {
      this.rangeStart = this.parseDate(value.start);
      this.rangeEnd = this.parseDate(value.end);
      this.render();
    } else if (this.mode === 'multiple' && Array.isArray(value)) {
      this.selectedDates = new Set(value);
      this.render();
    }
  }
}

/* ═══ REGISTER COMPONENT ═══ */
if (!customElements.get('ren-calendar')) {
  customElements.define('ren-calendar', RenCalendar);
}


id: file:components/composites/ren-carousel/ren-carousel.css
type: css
name: ren-carousel.css
path: components/composites/ren-carousel/ren-carousel.css
data: {}
body:
/* ═══════════════════════════════════════════════════════════════════════════
   RenDS — <ren-carousel> Web Component
   ═══════════════════════════════════════════════════════════════════════════
   Accessible carousel/slider with scroll-snap, keyboard nav, autoplay, and dots.

   Features:
   - CSS scroll-snap for performant sliding
   - Configurable slides-per-view (1, 2, 3, 4)
   - Fade or slide transitions
   - Touch-friendly swipe support
   - Keyboard navigation (Arrow keys)
   - Optional autoplay with progress indicator
   - Responsive via container queries
   - Respects prefers-reduced-motion
   ═════════════════════════════════════════════════════════════════════════ */

/* ═══ CAROUSEL OUTER CONTAINER ═══ */
.ren-carousel {
  container-type: inline-size;
  container-name: ren-carousel;
  position: relative;
  width: 100%;
  height: auto;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

/* ═══ VIEWPORT (SCROLL CONTAINER) ═══ */
.ren-carousel-viewport {
  position: relative;
  width: 100%;
  height: auto;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-behavior: smooth;
  scroll-snap-type: x mandatory;
  scrollbar-width: none; /* Firefox */
  -ms-overflow-style: none; /* IE and Edge */
  touch-action: pan-x pinch-zoom; /* Prevent vertical scroll interference during horizontal swipe */
}

/* Hide scrollbar in Chrome, Safari, and Opera */
.ren-carousel-viewport::-webkit-scrollbar {
  display: none;
}

/* Container for slides with flexbox */
.ren-carousel-viewport {
  display: flex;
  flex-wrap: nowrap;
}

/* ═══ INDIVIDUAL SLIDES ═══ */
.ren-carousel-slide {
  flex: 0 0 100%;
  min-width: 100%;
  scroll-snap-align: start;
  scroll-snap-stop: always;
  height: auto;
}

/* Ensure slide content can be sized */
.ren-carousel-slide > * {
  display: block;
  width: 100%;
  height: auto;
}

/* ═══ SLIDES-PER-VIEW VARIANTS ═══ */

/* 2 slides visible (50% each) */
.ren-carousel-2 .ren-carousel-slide {
  flex: 0 0 50%;
  min-width: 50%;
}

/* 3 slides visible (33.33% each) */
.ren-carousel-3 .ren-carousel-slide {
  flex: 0 0 calc(100% / 3);
  min-width: calc(100% / 3);
}

/* 4 slides visible (25% each) */
.ren-carousel-4 .ren-carousel-slide {
  flex: 0 0 25%;
  min-width: 25%;
}

/* Responsive: stack to 1 or 2 on narrow screens via container queries */
@supports (container-type: inline-size) {
  @container (max-width: 640px) {
    .ren-carousel-2 .ren-carousel-slide,
    .ren-carousel-3 .ren-carousel-slide,
    .ren-carousel-4 .ren-carousel-slide {
      flex: 0 0 100%;
      min-width: 100%;
    }
  }

  @container (max-width: 1024px) and (min-width: 641px) {
    .ren-carousel-3 .ren-carousel-slide,
    .ren-carousel-4 .ren-carousel-slide {
      flex: 0 0 50%;
      min-width: 50%;
    }
  }
}

/* ═══ FADE TRANSITION VARIANT ═══ */
.ren-carousel-fade {
  position: relative;
}

.ren-carousel-fade .ren-carousel-viewport {
  scroll-behavior: auto;
}

.ren-carousel-fade .ren-carousel-slide {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  opacity: 0;
  transition: opacity var(--duration-enter) var(--ease-enter);
  pointer-events: none;
}

.ren-carousel-fade .ren-carousel-slide[aria-current="true"] {
  position: relative;
  opacity: 1;
  pointer-events: auto;
}

/* ═══ NAVIGATION ARROWS ═══ */
.ren-carousel-prev,
.ren-carousel-next {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 10;

  width: var(--touch-min);
  height: var(--touch-min);
  min-width: 44px;
  min-height: 44px;

  display: flex;
  align-items: center;
  justify-content: center;

  background-color: rgb(0, 0, 0, 0.4);
  border: none;
  border-radius: var(--radius-full);
  cursor: pointer;
  color: white;
  font-size: 20px;
  transition: var(--transition-tactile);
}

/* Arrow positioning */
.ren-carousel-prev {
  left: var(--space-3);
}

.ren-carousel-next {
  right: var(--space-3);
}

/* Arrow chevron using ::before pseudo-element */
.ren-carousel-prev::before {
  content: '‹';
  font-size: 24px;
  font-weight: bold;
  line-height: 1;
}

.ren-carousel-next::before {
  content: '›';
  font-size: 24px;
  font-weight: bold;
  line-height: 1;
}

/* Hover state — increase opacity */
.ren-carousel-prev:hover:not(:disabled),
.ren-carousel-next:hover:not(:disabled) {
  background-color: rgb(0, 0, 0, 0.7);
}

/* Active/focus state */
.ren-carousel-prev:focus-visible,
.ren-carousel-next:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* Hidden arrows at boundaries */
.ren-carousel[data-at-start] .ren-carousel-prev,
.ren-carousel[data-at-end] .ren-carousel-next {
  opacity: 0;
  pointer-events: none;
}

/* Disabled state */
.ren-carousel-prev:disabled,
.ren-carousel-next:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

/* ═══ PROGRESSIVE ENHANCEMENT: CSS-ONLY SCROLL BUTTONS ═══
   Modern browsers support ::scroll-button() pseudo-elements for native
   scroll container navigation without JavaScript.
   When supported, these replace the JS-generated arrow buttons. */

@supports selector(::scroll-button(inline-start)) {
  .ren-carousel-viewport {
    /* Enable native scroll buttons */
    overflow-x: auto;
  }

  .ren-carousel-viewport::scroll-button(inline-start),
  .ren-carousel-viewport::scroll-button(inline-end) {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    z-index: 10;

    width: var(--touch-min, 44px);
    height: var(--touch-min, 44px);
    min-width: 44px;
    min-height: 44px;

    display: flex;
    align-items: center;
    justify-content: center;

    background-color: rgb(0, 0, 0, 0.4);
    border: none;
    border-radius: var(--radius-full, 9999px);
    cursor: pointer;
    color: white;

    transition: var(--transition-tactile);
  }

  .ren-carousel-viewport::scroll-button(inline-start) {
    content: '‹';
    left: var(--space-3, 0.75rem);
  }

  .ren-carousel-viewport::scroll-button(inline-end) {
    content: '›';
    right: var(--space-3, 0.75rem);
  }

  .ren-carousel-viewport::scroll-button(inline-start):hover,
  .ren-carousel-viewport::scroll-button(inline-end):hover {
    background-color: rgb(0, 0, 0, 0.7);
  }

  /* Hide JS-generated arrows when native scroll buttons are available */
  .ren-carousel .ren-carousel-prev,
  .ren-carousel .ren-carousel-next {
    display: none;
  }
}

/* ═══ PAGINATION DOTS ═══ */
.ren-carousel-dots {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  margin-top: var(--space-3);
  padding: 0;
  list-style: none;
}

.ren-carousel-dot {
  width: 8px;
  height: 8px;
  border-radius: var(--radius-full);
  background-color: var(--color-border);
  border: none;
  cursor: pointer;
  transition: var(--transition-tactile);
  padding: 0;
  margin: 0;
}

/* Active dot */
.ren-carousel-dot[aria-current="true"] {
  background-color: var(--color-accent);
  transform: scale(1.25);
}

/* Hover state */
.ren-carousel-dot:hover:not([aria-current="true"]) {
  background-color: var(--color-border);
  opacity: 0.7;
}

/* Focus visible */
.ren-carousel-dot:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
  border-radius: var(--radius-full);
}

/* ═══ AUTOPLAY PROGRESS BAR ═══ */
.ren-carousel-dot[aria-current="true"]::after {
  content: '';
  position: absolute;
  bottom: -4px;
  left: 50%;
  transform: translateX(-50%);
  width: 6px;
  height: 2px;
  background-color: var(--color-accent);
  border-radius: var(--radius-full);
  opacity: 0;
}

/* Show progress bar when autoplay is active */
.ren-carousel[data-autoplay] .ren-carousel-dot[aria-current="true"]::after {
  opacity: 1;
  animation: carousel-progress linear forwards;
}

@keyframes carousel-progress {
  from {
    width: 0;
  }
  to {
    width: 6px;
  }
}

/* Dynamic progress bar timing — controlled by JS */
.ren-carousel[data-autoplay-duration] .ren-carousel-dot[aria-current="true"]::after {
  animation-duration: inherit;
}

/* ═══ CAROUSEL COUNTER ═══ */
.ren-carousel-counter {
  text-align: center;
  font-size: 14px;
  color: var(--color-text-muted);
  margin-top: var(--space-2);
  font-variant-numeric: tabular-nums;
}

.ren-carousel-counter-current {
  font-weight: 600;
  color: var(--color-text);
}

/* ═══ ACCESSIBILITY & ANIMATIONS ═══ */

/* Respect reduced motion preference */
@media (prefers-reduced-motion: reduce) {
  .ren-carousel-viewport {
    scroll-behavior: auto;
  }

  .ren-carousel-slide,
  .ren-carousel-dot,
  .ren-carousel-prev,
  .ren-carousel-next {
    transition: none;
    animation: none;
  }

  .ren-carousel[data-autoplay] .ren-carousel-dot[aria-current="true"]::after {
    animation: none;
    width: 6px;
    opacity: 1;
  }
}

/* ═══ FOCUS VISIBLE FOR KEYBOARD NAVIGATION ═══ */
.ren-carousel-slide:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* ═══ RESPONSIVE ADJUSTMENTS (Container Queries) ═══ */

/* Compact carousel */
@container ren-carousel (max-width: 480px) {
  .ren-carousel {
    gap: var(--space-2);
  }

  .ren-carousel-prev,
  .ren-carousel-next {
    width: 40px;
    height: 40px;
    font-size: 16px;
  }

  .ren-carousel-prev {
    left: var(--space-2);
  }

  .ren-carousel-next {
    right: var(--space-2);
  }

  .ren-carousel-dots {
    gap: var(--space-1);
  }

  .ren-carousel-dot {
    width: 6px;
    height: 6px;
  }

  .ren-carousel-dot[aria-current="true"] {
    transform: scale(1.1);
  }
}

/* Wide carousel — hover reveal arrows */
@container ren-carousel (min-width: 768px) {
  .ren-carousel-prev,
  .ren-carousel-next {
    opacity: 0.6;
  }

  .ren-carousel:hover .ren-carousel-prev:not(:disabled),
  .ren-carousel:hover .ren-carousel-next:not(:disabled) {
    opacity: 1;
  }
}

/* ═══ UTILITY CLASSES ═══ */

/* Carousel without dots */
.ren-carousel-no-dots .ren-carousel-dots {
  display: none;
}

/* Carousel without arrows */
.ren-carousel-no-arrows .ren-carousel-prev,
.ren-carousel-no-arrows .ren-carousel-next {
  display: none;
}

/* Carousel without counter */
.ren-carousel-no-counter .ren-carousel-counter {
  display: none;
}

/* Full width slides (1 per view) */
.ren-carousel-full .ren-carousel-slide {
  flex: 0 0 100%;
  min-width: 100%;
}

/* Draggable appearance */
.ren-carousel-viewport {
  user-select: none;
  -webkit-user-select: none;
}

.ren-carousel-slide {
  user-select: none;
  -webkit-user-select: none;
}

/* Touch feedback */
.ren-carousel-slide {
  cursor: grab;
}

.ren-carousel-slide:active {
  cursor: grabbing;
}

/* ═══ LOADING STATE ═══ */
.ren-carousel[aria-busy="true"] {
  opacity: 0.6;
  pointer-events: none;
}

/* ═══ SEMANTIC HTML ADJUSTMENTS ═══ */
.ren-carousel[role="group"],
.ren-carousel[role="region"] {
  /* Already defined above, this is for clarity */
}

/* Ensure images in slides maintain aspect ratio */
.ren-carousel-slide img {
  width: 100%;
  height: auto;
  display: block;
  object-fit: cover;
}

/* Video in carousel */
.ren-carousel-slide video {
  width: 100%;
  height: auto;
  display: block;
}
/* Appearance API bridge */
.ren-carousel { gap: var(--ren-carousel-gap, var(--space-4)); border-radius: var(--ren-carousel-radius, var(--radius-lg)); transition-duration: var(--ren-carousel-duration, var(--duration-normal)); transition-timing-function: var(--ren-carousel-easing, var(--ease-out)); }
.ren-carousel .ren-carousel-arrow { inline-size: var(--ren-carousel-arrow-size, var(--size-lg)); block-size: var(--ren-carousel-arrow-size, var(--size-lg)); }
.ren-carousel .ren-carousel-dot { inline-size: var(--ren-carousel-dot-size, 8px); block-size: var(--ren-carousel-dot-size, 8px); background: var(--ren-carousel-dot-color, var(--color-fill)); }
.ren-carousel .ren-carousel-dot[aria-current="true"], .ren-carousel .ren-carousel-dot.is-active { background: var(--ren-carousel-dot-active, var(--color-accent)); }


id: file:components/composites/ren-carousel/ren-carousel.js
type: javascript
name: ren-carousel.js
path: components/composites/ren-carousel/ren-carousel.js
data: {}
body:
/**
 * RenDS — <ren-carousel> Web Component
 * ═════════════════════════════════════════════════════════════════════════
 * Accessible carousel/slider with native scroll-snap, keyboard navigation,
 * autoplay, pagination dots, and optional next/prev arrows.
 *
 * Features:
 * - Light DOM (no Shadow DOM)
 * - CSS scroll-snap for performant horizontal scrolling
 * - Keyboard navigation (Arrow Left/Right)
 * - Touch swipe support (native scroll-snap)
 * - Auto-generated pagination dots
 * - Optional next/prev navigation arrows
 * - Autoplay with configurable interval
 * - Loop mode (infinite carousel)
 * - Configurable slides-per-view (1, 2, 3, 4)
 * - Fade transition variant
 * - Custom events (ren-slide-change)
 * - Respects prefers-reduced-motion
 *
 * Attributes:
 *   autoplay: milliseconds interval for autoplay (e.g., 5000)
 *   loop: enable infinite looping
 *   slides-per-view: number of slides visible (default: 1)
 *   fade: enable fade transition instead of slide
 *
 * Methods:
 *   next() — go to next slide
 *   prev() — go to previous slide
 *   goTo(index) — jump to specific slide
 *   pause() — pause autoplay
 *   resume() — resume autoplay
 *
 * Events:
 *   ren-slide-change: detail = { index, total, id }
 *
 * Markup:
 *   <ren-carousel autoplay="5000" loop slides-per-view="1">
 *     <div class="ren-carousel-viewport">
 *       <div class="ren-carousel-slide">
 *         <img src="slide1.jpg" alt="Slide 1">
 *       </div>
 *       <div class="ren-carousel-slide">
 *         <img src="slide2.jpg" alt="Slide 2">
 *       </div>
 *       <!-- Dots and arrows auto-generated by JS -->
 *     </div>
 *   </ren-carousel>
 * ═════════════════════════════════════════════════════════════════════════
 */

let nextId = 0;

export class RenCarousel extends HTMLElement {
  static get observedAttributes() {
    return ['autoplay', 'loop', 'slides-per-view', 'fade'];
  }

  constructor() {
    super();
    this.id = this.id || `ren-carousel-${++nextId}`;

    this._viewport = null;
    this._slides = [];
    this._dotsContainer = null;
    this._dots = [];
    this._prevButton = null;
    this._nextButton = null;
    this._counterElement = null;

    this._currentIndex = 0;
    this._totalSlides = 0;
    this._autoplayInterval = null;
    this._autoplayDuration = null;
    this._isAutoplayActive = false;
    this._scrollTimeout = null;
    this._intersectionObserver = null;
    this._contentObserver = null;
    this._refreshQueued = false;
    this._nextSlideId = 0;

    this._autoplayMs = 0;
    this._shouldLoop = false;
    this._slidePerView = 1;
    this._isFade = false;
    this._reducedMotionQuery = null;
    this._handleReducedMotionChange = null;
  }

  /* ═══ LIFECYCLE ═══ */

  connectedCallback() {
    this._parseAttributes();
    this._initialize();
    this._observeContent();
    this._attachReducedMotionListener();
    this._attachEventListeners();
    if (this._autoplayMs > 0) {
      this._startAutoplay();
    }
  }

  disconnectedCallback() {
    this._stopAutoplay();
    if (this._intersectionObserver) {
      this._intersectionObserver.disconnect();
    }
    this._contentObserver?.disconnect();
    this._contentObserver = null;
    this._detachReducedMotionListener();
    this._detachEventListeners();
  }

  _observeContent() {
    if (typeof MutationObserver === 'undefined' || this._contentObserver) return;
    this._contentObserver = new MutationObserver((mutations) => {
      // Ignore mutations caused by our generated controls; refresh only when
      // slide/viewport content actually changes.
      if (!mutations.some((m) => [...m.addedNodes, ...m.removedNodes].some(
        (node) => node.nodeType === Node.ELEMENT_NODE &&
          (node.classList?.contains('ren-carousel-slide') ||
            node.classList?.contains('ren-carousel-viewport'))
      ))) return;
      if (this._refreshQueued) return;
      this._refreshQueued = true;
      queueMicrotask(() => {
        this._refreshQueued = false;
        if (!this.isConnected) return;
        this._contentObserver?.disconnect();
        this._initialize();
        this._contentObserver?.observe(this, { childList: true, subtree: true });
      });
    });
    this._contentObserver.observe(this, { childList: true, subtree: true });
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (!this.isConnected) return;

    switch (name) {
      case 'autoplay':
        this._autoplayMs = newValue ? parseInt(newValue, 10) : 0;
        if (this._autoplayMs > 0) {
          this._startAutoplay();
        } else {
          this._stopAutoplay();
        }
        break;
      case 'loop':
        this._shouldLoop = this.hasAttribute('loop');
        this._updateArrowVisibility();
        break;
      case 'slides-per-view':
        this._slidePerView = newValue ? parseInt(newValue, 10) : 1;
        this._applySlidePerViewClass();
        break;
      case 'fade':
        this._isFade = this.hasAttribute('fade');
        this._updateFadeVariant();
        this._setupIntersectionObserver();
        break;
    }
  }

  /* ═══ INITIALIZATION ═══ */

  _parseAttributes() {
    this._autoplayMs = this.hasAttribute('autoplay')
      ? parseInt(this.getAttribute('autoplay'), 10)
      : 0;
    this._shouldLoop = this.hasAttribute('loop');
    this._slidePerView = this.hasAttribute('slides-per-view')
      ? parseInt(this.getAttribute('slides-per-view'), 10)
      : 1;
    this._isFade = this.hasAttribute('fade');
  }

  _initialize() {
    // Rebuilds must tear down observers and generated navigation first. This
    // keeps removal-to-empty and mode changes from retaining stale state.
    this._intersectionObserver?.disconnect();
    this._intersectionObserver = null;
    this.querySelector('.ren-carousel-dots')?.remove();
    this.querySelectorAll('.ren-carousel-prev, .ren-carousel-next').forEach((el) => el.remove());
    this.querySelector('.ren-carousel-counter')?.remove();
    // Set up ARIA attributes
    this.setAttribute('role', 'group');
    this.setAttribute('aria-roledescription', 'carousel');
    if (!this.getAttribute('aria-label')) {
      this.setAttribute('aria-label', 'Image carousel');
    }

    // Find viewport
    this._viewport = this.querySelector('.ren-carousel-viewport');
    if (!this._viewport) {
      console.warn('RenCarousel: No viewport found');
      return;
    }

    // Add scroll-snap to viewport if not already present
    if (!this._viewport.classList.contains('ren-carousel-viewport')) {
      this._viewport.classList.add('ren-carousel-viewport');
    }

    // Collect slides
    this._slides = Array.from(
      this.querySelectorAll('.ren-carousel-slide')
    );

    if (this._slides.length === 0) {
      this._totalSlides = 0;
      this._currentIndex = 0;
      this._dots = [];
      this._dotsContainer = null;
      this._prevButton = null;
      this._nextButton = null;
      this._counterElement = null;
      console.warn('RenCarousel: No slides found');
      return;
    }

    this._totalSlides = this._slides.length;
    this._currentIndex = Math.max(0, Math.min(this._currentIndex, this._totalSlides - 1));

    // Set up slides with ARIA attributes
    this._slides.forEach((slide, index) => {
      if (!slide.id) {
        let generatedId;
        do {
          generatedId = `${this.id}-slide-${++this._nextSlideId}`;
        } while (
          this._slides.some((candidate) => candidate !== slide && candidate.id === generatedId) ||
          this.ownerDocument?.getElementById(generatedId)
        );
        slide.id = generatedId;
      }
      slide.setAttribute('role', 'group');
      slide.setAttribute('aria-roledescription', 'slide');
      slide.setAttribute('aria-label', `Slide ${index + 1} of ${this._totalSlides}`);
      if (index === this._currentIndex) {
        slide.setAttribute('aria-current', 'true');
      } else {
        slide.removeAttribute('aria-current');
      }
      slide.tabIndex = 0;
    });

    // Create navigation elements
    this._createDots();
    this._createArrows();
    this._createCounter();

    // Apply variant classes
    this._applySlidePerViewClass();
    this._updateFadeVariant();

    // Set up Intersection Observer to track active slide
    this._setupIntersectionObserver();

    // Initial state
    this._updateArrowVisibility();
    this._updateDotState();
    this._updateCounter();
  }

  _createDots() {
    // Remove existing dots if any
    const existing = this.querySelector('.ren-carousel-dots');
    if (existing) existing.remove();

    // Create dots container
    this._dotsContainer = document.createElement('ul');
    this._dotsContainer.className = 'ren-carousel-dots';
    this._dotsContainer.setAttribute('aria-label', 'Carousel pagination');

    // Create individual dots
    this._dots = this._slides.map((_, index) => {
      const dot = document.createElement('button');
      dot.className = 'ren-carousel-dot';
      dot.setAttribute('type', 'button');
      dot.setAttribute('aria-label', `Go to slide ${index + 1}`);
      dot.setAttribute('aria-controls', this._slides[index].id);
      if (index === 0) {
        dot.setAttribute('aria-current', 'true');
      }

      dot.addEventListener('click', () => this.goTo(index));
      const li = document.createElement('li');
      li.appendChild(dot);
      this._dotsContainer.appendChild(li);

      return dot;
    });

    this.appendChild(this._dotsContainer);
  }

  _createArrows() {
    // Remove existing arrows if any
    const existing = this.querySelectorAll('.ren-carousel-prev, .ren-carousel-next');
    existing.forEach(el => el.remove());

    // Create previous button
    this._prevButton = document.createElement('button');
    this._prevButton.className = 'ren-carousel-prev';
    this._prevButton.setAttribute('type', 'button');
    this._prevButton.setAttribute('aria-label', 'Previous slide');
    this._prevButton.addEventListener('click', () => this.prev());

    // Create next button
    this._nextButton = document.createElement('button');
    this._nextButton.className = 'ren-carousel-next';
    this._nextButton.setAttribute('type', 'button');
    this._nextButton.setAttribute('aria-label', 'Next slide');
    this._nextButton.addEventListener('click', () => this.next());

    // Insert after viewport
    this._viewport.parentNode.insertBefore(this._prevButton, this._viewport.nextSibling);
    this._viewport.parentNode.insertBefore(this._nextButton, this._viewport.nextSibling.nextSibling);
  }

  _createCounter() {
    // Remove existing counter if any
    const existing = this.querySelector('.ren-carousel-counter');
    if (existing) existing.remove();

    this._counterElement = document.createElement('div');
    this._counterElement.className = 'ren-carousel-counter';
    this._counterElement.setAttribute('aria-atomic', 'true');
    this._syncLiveRegion();

    this.appendChild(this._counterElement);
  }

  _applySlidePerViewClass() {
    // Remove all slides-per-view classes
    this.classList.remove('ren-carousel-1', 'ren-carousel-2', 'ren-carousel-3', 'ren-carousel-4');

    // Add the appropriate class
    if (this._slidePerView >= 1 && this._slidePerView <= 4) {
      this.classList.add(`ren-carousel-${this._slidePerView}`);
    }
  }

  _updateFadeVariant() {
    if (this._isFade) {
      this.classList.add('ren-carousel-fade');
    } else {
      this.classList.remove('ren-carousel-fade');
    }
  }

  _setupIntersectionObserver() {
    if (this._intersectionObserver) {
      this._intersectionObserver.disconnect();
      this._intersectionObserver = null;
    }
    if (this._isFade) {
      // For fade mode, we'll handle active state differently
      return;
    }

    const options = {
      root: this._viewport,
      threshold: 0.5,
    };

    this._intersectionObserver = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          const index = this._slides.indexOf(entry.target);
          if (index !== -1) {
            this._setCurrentIndex(index);
          }
        }
      });
    }, options);

    this._slides.forEach((slide) => {
      this._intersectionObserver.observe(slide);
    });
  }

  /* ═══ NAVIGATION METHODS ═══ */

  next() {
    if (this._currentIndex < this._totalSlides - 1) {
      this.goTo(this._currentIndex + 1);
    } else if (this._shouldLoop) {
      this.goTo(0);
    }
  }

  prev() {
    if (this._currentIndex > 0) {
      this.goTo(this._currentIndex - 1);
    } else if (this._shouldLoop) {
      this.goTo(this._totalSlides - 1);
    }
  }

  goTo(index) {
    // Clamp index
    index = Math.max(0, Math.min(index, this._totalSlides - 1));

    // Scroll to slide
    const slide = this._slides[index];
    if (slide) {
      slide.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'start' });
      this._setCurrentIndex(index);

      // Reset autoplay timer when navigating
      if (this._autoplayMs > 0) {
        this._stopAutoplay();
        this._startAutoplay();
      }
    }
  }

  _setCurrentIndex(index) {
    if (this._currentIndex === index) return;

    // Update slides ARIA
    this._slides.forEach((slide, i) => {
      if (i === index) {
        slide.setAttribute('aria-current', 'true');
      } else {
        slide.removeAttribute('aria-current');
      }
    });

    this._currentIndex = index;
    this._updateDotState();
    this._updateArrowVisibility();
    this._updateCounter();
    this._dispatchChangeEvent();
  }

  /* ═══ STATE UPDATES ═══ */

  _updateDotState() {
    this._dots.forEach((dot, index) => {
      if (index === this._currentIndex) {
        dot.setAttribute('aria-current', 'true');
      } else {
        dot.removeAttribute('aria-current');
      }
    });
  }

  _updateArrowVisibility() {
    if (this._prevButton && this._nextButton) {
      const atStart = this._currentIndex === 0;
      const atEnd = this._currentIndex === this._totalSlides - 1;

      if (this._shouldLoop) {
        // When looping, arrows are always visible
        this.removeAttribute('data-at-start');
        this.removeAttribute('data-at-end');
        this._prevButton.removeAttribute('disabled');
        this._nextButton.removeAttribute('disabled');
      } else {
        // Without loop, hide/disable arrows at boundaries
        if (atStart) {
          this.setAttribute('data-at-start', '');
          this._prevButton.setAttribute('disabled', '');
        } else {
          this.removeAttribute('data-at-start');
          this._prevButton.removeAttribute('disabled');
        }

        if (atEnd) {
          this.setAttribute('data-at-end', '');
          this._nextButton.setAttribute('disabled', '');
        } else {
          this.removeAttribute('data-at-end');
          this._nextButton.removeAttribute('disabled');
        }
      }
    }
  }

  _updateCounter() {
    if (this._counterElement) {
      this._counterElement.innerHTML = `
        <span class="ren-carousel-counter-current">${this._currentIndex + 1}</span>
        <span> / ${this._totalSlides}</span>
      `;
    }
  }

  _syncLiveRegion() {
    this._counterElement?.setAttribute('aria-live', this._isAutoplayActive ? 'off' : 'polite');
  }

  /* ═══ AUTOPLAY ═══ */

  _prefersReducedMotion() {
    return window.matchMedia?.('(prefers-reduced-motion: reduce)').matches === true;
  }

  _attachReducedMotionListener() {
    this._reducedMotionQuery = window.matchMedia?.('(prefers-reduced-motion: reduce)') ?? null;
    if (!this._reducedMotionQuery) return;

    this._handleReducedMotionChange = (event) => {
      if (event.matches) {
        this._stopAutoplay();
      } else if (this._autoplayMs > 0) {
        this._startAutoplay();
      }
    };

    this._reducedMotionQuery.addEventListener?.('change', this._handleReducedMotionChange);
  }

  _detachReducedMotionListener() {
    if (this._reducedMotionQuery && this._handleReducedMotionChange) {
      this._reducedMotionQuery.removeEventListener?.('change', this._handleReducedMotionChange);
    }
    this._reducedMotionQuery = null;
    this._handleReducedMotionChange = null;
  }

  _startAutoplay() {
    if (this._autoplayInterval || this._autoplayMs <= 0) return;
    if (this._prefersReducedMotion()) {
      this._stopAutoplay();
      return;
    }

    this._isAutoplayActive = true;
    this.setAttribute('data-autoplay', '');
    this.setAttribute('data-autoplay-duration', `${this._autoplayMs}ms`);
    this._syncLiveRegion();

    // Start autoplay
    this._autoplayInterval = setInterval(() => {
      this.next();
    }, this._autoplayMs);
  }

  _stopAutoplay() {
    if (this._autoplayInterval) {
      clearInterval(this._autoplayInterval);
      this._autoplayInterval = null;
    }
    this._isAutoplayActive = false;
    this.removeAttribute('data-autoplay');
    this.removeAttribute('data-autoplay-duration');
    this._syncLiveRegion();
  }

  pause() {
    this._stopAutoplay();
  }

  resume() {
    if (this._autoplayMs > 0) {
      this._startAutoplay();
    }
  }

  /* ═══ EVENT LISTENERS ═══ */

  _attachEventListeners() {
    // Keyboard navigation
    this._handleKeydown = (e) => this._onKeydown(e);
    this.addEventListener('keydown', this._handleKeydown);

    // Pause autoplay on hover
    this._handleMouseEnter = () => {
      if (this._isAutoplayActive) {
        this._stopAutoplay();
      }
    };
    this._handleMouseLeave = () => {
      if (this._autoplayMs > 0) {
        this._startAutoplay();
      }
    };
    this.addEventListener('mouseenter', this._handleMouseEnter);
    this.addEventListener('mouseleave', this._handleMouseLeave);

    // Pause autoplay on focus
    this._handleFocus = () => {
      if (this._isAutoplayActive) {
        this._stopAutoplay();
      }
    };
    this._handleBlur = () => {
      if (this._autoplayMs > 0) {
        this._startAutoplay();
      }
    };
    this.addEventListener('focus', this._handleFocus, true);
    this.addEventListener('blur', this._handleBlur, true);
  }

  _detachEventListeners() {
    if (this._handleKeydown) {
      this.removeEventListener('keydown', this._handleKeydown);
    }
    if (this._handleMouseEnter) {
      this.removeEventListener('mouseenter', this._handleMouseEnter);
    }
    if (this._handleMouseLeave) {
      this.removeEventListener('mouseleave', this._handleMouseLeave);
    }
    if (this._handleFocus) {
      this.removeEventListener('focus', this._handleFocus, true);
    }
    if (this._handleBlur) {
      this.removeEventListener('blur', this._handleBlur, true);
    }
  }

  _onKeydown(e) {
    if (e.key === 'ArrowLeft') {
      e.preventDefault();
      this.prev();
    } else if (e.key === 'ArrowRight') {
      e.preventDefault();
      this.next();
    } else if (e.key === ' ' || e.key === 'Enter') {
      // Allow space/enter on dots
      if (e.target.classList.contains('ren-carousel-dot')) {
        e.preventDefault();
        e.target.click();
      }
    }
  }

  /* ═══ EVENT DISPATCHING ═══ */

  _dispatchChangeEvent() {
    const event = new CustomEvent('ren-slide-change', {
      detail: {
        index: this._currentIndex,
        total: this._totalSlides,
        id: this.id,
      },
      bubbles: true,
      composed: true,
    });
    this.dispatchEvent(event);
  }
}

// Register the component
if (!customElements.get('ren-carousel')) {
  customElements.define('ren-carousel', RenCarousel);
}


id: file:components/composites/ren-collapsible/ren-collapsible.css
type: css
name: ren-collapsible.css
path: components/composites/ren-collapsible/ren-collapsible.css
data: {}
body:
/* ============================================
   RenDS — Collapsible Component
   ============================================
   Expand/collapse content panel.
   Uses native <details>/<summary> — ZERO JS.

   CSS-only animation with interpolate-size.

   Different from Accordion: standalone, no group
   exclusivity. For single expand/collapse sections.

   Usage:
     <details class="ren-collapsible">
       <summary>Click to expand</summary>
       <div class="ren-collapsible-content">
         Hidden content here
       </div>
     </details>

   Open by default:
     <details class="ren-collapsible" open>
       ...
     </details>
   ============================================ */

/* Enable height animation on details */
.ren-collapsible {
  interpolate-size: allow-keywords;
  border: var(--stroke-1) solid var(--color-border);
  border-radius: var(--radius-md);
  overflow: hidden;
}

/* Summary (trigger) */
.ren-collapsible > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  min-height: var(--touch-min);
  padding: var(--space-3) var(--space-4);
  font-size: var(--body-size);
  font-weight: var(--weight-medium);
  color: var(--color-text);
  cursor: pointer;
  user-select: none;
  list-style: none; /* Remove default marker */
  transition: var(--transition-tactile);
}

/* Remove default marker in WebKit */
.ren-collapsible > summary::-webkit-details-marker {
  display: none;
}

/* Custom chevron */
.ren-collapsible > summary::after {
  content: '';
  width: 0.5rem;
  height: 0.5rem;
  border: solid var(--color-text-muted);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
  flex-shrink: 0;
  transition: transform var(--duration-enter) var(--ease-enter);
}

.ren-collapsible[open] > summary::after {
  transform: rotate(-135deg);
}

.ren-collapsible > summary:hover {
  background-color: var(--color-fill);
}

.ren-collapsible > summary:focus-visible {
  outline: var(--ring-width) solid var(--color-focus-ring);
  outline-offset: -2px;
}

/* Content area */
.ren-collapsible-content {
  padding: 0 var(--space-4) var(--space-4);
  font-size: var(--body-size);
  color: var(--color-text-secondary);
  line-height: var(--leading-normal);
}

/* ─── Ghost variant (no border) ─── */
.ren-collapsible-ghost {
  border: none;
}

.ren-collapsible-ghost > summary {
  padding-inline: 0;
}

.ren-collapsible-ghost .ren-collapsible-content {
  padding-inline: 0;
}

/* ─── Animate open/close ─── */
/* Uses CSS content-visibility for animation */
.ren-collapsible .ren-collapsible-content {
  content-visibility: visible;
}

/* Reduced motion — no animation */
@media (prefers-reduced-motion: reduce) {
  .ren-collapsible > summary::after {
    transition: none;
  }
}
/* Appearance API bridge */
.ren-collapsible, .ren-accordion { border-color: var(--ren-collapse-border, var(--color-border)); border-radius: var(--ren-collapse-radius, var(--radius-lg)); background: var(--ren-collapse-bg, var(--color-surface)); transition-duration: var(--ren-collapse-duration, var(--duration-normal)); transition-timing-function: var(--ren-collapse-easing, var(--ease-out)); }
.ren-collapsible > summary, .ren-accordion > summary { padding: var(--ren-collapse-padding, var(--space-4)); font-size: var(--ren-collapse-trigger-font, var(--label-lg-size)); font-weight: var(--ren-collapse-trigger-weight, var(--weight-semibold)); }


id: file:components/composites/ren-color-picker/ren-color-picker.css
type: css
name: ren-color-picker.css
path: components/composites/ren-color-picker/ren-color-picker.css
data: {}
body:
/* ═══════════════════════════════════════════════════════════════════
   RenDS — Color Picker Composite Component
   ═══════════════════════════════════════════════════════════════════
   A fully accessible, feature-rich color picker with support for:
   - HSV color model (internal) with conversion to/from HEX, RGB, HSL
   - Saturation/brightness canvas area
   - Hue slider
   - Optional alpha/opacity slider
   - Text inputs for manual color entry
   - Preset color swatches
   - EyeDropper API integration (when available)
   - Keyboard navigation and ARIA labels
   - Smooth animations with @starting-style support

   Structure:
   .ren-color-picker (container)
     .ren-color-picker-trigger (button showing current color)
     [popover] .ren-color-picker-dropdown
       .ren-color-picker-saturation (canvas area for S/B)
         .ren-color-picker-saturation-handle (circular cursor)
       .ren-color-picker-hue (horizontal rainbow slider)
         .ren-color-picker-hue-handle (thumb)
       .ren-color-picker-alpha (opacity slider, optional)
         .ren-color-picker-alpha-handle (thumb)
       .ren-color-picker-preview (color display swatch)
       .ren-color-picker-inputs
         .ren-color-picker-hex-input
         .ren-color-picker-channel (R/G/B or H/S/L)
       .ren-color-picker-swatches (preset colors, optional)
       .ren-color-picker-format-toggle (HEX/RGB/HSL switcher)
       .ren-color-picker-eyedropper (screen picker, optional)
   ═══════════════════════════════════════════════════════════════════ */

/* ═══ COLOR PICKER CONTAINER ═══ */
.ren-color-picker {
  position: relative;
  display: inline-flex;
  flex-direction: column;
  gap: var(--space-2);
  font-family: inherit;
}

/* ═══ TRIGGER BUTTON (Shows Current Color) ═══ */
.ren-color-picker-trigger {
  anchor-name: --ren-color-picker-anchor;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  width: 100%;
  min-height: var(--touch-min);
  padding: var(--space-2) var(--space-3);
  font-size: var(--body-size);
  font-family: inherit;
  color: var(--color-text);
  background-color: var(--color-input-bg);
  border: 1px solid var(--color-input-border);
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: var(--transition-tactile);
}

.ren-color-picker-trigger:hover:not(:disabled) {
  background-color: var(--color-input-bg);
  border-color: var(--color-input-border-focus);
}

.ren-color-picker-trigger:focus-visible {
  outline: none;
  border-color: var(--color-input-border-focus);
  box-shadow: 0 0 0 3px var(--color-input-focus-ring);
}

.ren-color-picker-trigger[aria-expanded="true"] {
  border-color: var(--color-input-border-focus);
  box-shadow: 0 0 0 3px var(--color-input-focus-ring);
}

.ren-color-picker-trigger:disabled {
  opacity: 0.5;
  cursor: not-allowed;
  pointer-events: none;
}

/* Trigger swatch (small colored square) */
.ren-color-picker-trigger-swatch {
  flex-shrink: 0;
  width: 24px;
  height: 24px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--color-border);
  background-color: var(--white);
  background-image: repeating-conic-gradient(
    var(--color-border) 0deg 90deg,
    transparent 90deg 180deg
  ) 50% / 4px 4px;
}

/* Trigger value text */
.ren-color-picker-trigger-value {
  flex: 1;
  text-align: start;
  font-family: var(--font-mono);
  font-size: var(--label-size);
}

/* ═══ DROPDOWN CONTAINER (Popover) ═══ */
.ren-color-picker-dropdown {
  position: absolute;
  position-anchor: --ren-color-picker-anchor;
  position-area: bottom span-all;
  position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
  inset: auto;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  width: auto;
  max-width: 300px;
  padding: var(--space-4);
  background-color: var(--color-surface-raised);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  z-index: 50;

  /* Popover API defaults */
  &:popover-open {
    animation: ren-color-picker-open var(--duration-enter) var(--ease-enter);
  }

  @supports (animation-timeline: view()) {
    &:popover-open {
      animation: ren-color-picker-open var(--duration-enter) var(--ease-enter);
      transition-behavior: allow-discrete;
    }

    @starting-style {
      opacity: 0;
      transform: scale(0.95);
    }
  }
}

.ren-color-picker-dropdown[data-side="bottom"],
.ren-color-picker-dropdown:not([data-side]) {
  position-area: bottom span-all;
  margin-block-start: var(--space-2);
  margin-block-end: 0;
  margin-inline: 0;
}

.ren-color-picker-dropdown[data-side="top"] {
  position-area: top span-all;
  margin-block-start: 0;
  margin-block-end: var(--space-2);
  margin-inline: 0;
}

.ren-color-picker-dropdown[data-side="right"] {
  position-area: right span-all;
  margin-block: 0;
  margin-inline-start: var(--space-2);
  margin-inline-end: 0;
}

.ren-color-picker-dropdown[data-side="left"] {
  position-area: left span-all;
  margin-block: 0;
  margin-inline-start: 0;
  margin-inline-end: var(--space-2);
}

@supports not ((anchor-name: --ren-anchor) and
  (position-anchor: --ren-anchor) and
  (position-area: bottom span-all)) {
  .ren-color-picker-dropdown {
    position: absolute;
    inset: auto;
  }
}

@keyframes ren-color-picker-open {
  from {
    opacity: 0;
    transform: scale(0.95);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

/* ═══ SATURATION/BRIGHTNESS AREA ═══ */
.ren-color-picker-saturation {
  position: relative;
  width: 100%;
  aspect-ratio: 1;
  max-width: 240px;
  border-radius: var(--radius-md);
  border: 1px solid var(--color-border);
  cursor: crosshair;
  overflow: hidden;
  user-select: none;
  -webkit-user-select: none;
}

/* Canvas for saturation/brightness */
.ren-color-picker-saturation-canvas {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: var(--radius-md);
}

/* Saturation handle (circular cursor) */
.ren-color-picker-saturation-handle {
  position: absolute;
  top: 0;
  left: 0;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  border: 2px solid white;
  box-shadow: 0 0 0 1px rgb(0, 0, 0, 0.3);
  pointer-events: none;
  transform: translate(-50%, -50%);
}

/* ═══ HUE SLIDER ═══ */
.ren-color-picker-hue {
  position: relative;
  width: 100%;
  height: 32px;
  border-radius: var(--radius-md);
  border: 1px solid var(--color-border);
  cursor: pointer;
  /* Hue rainbow. Intencionalmente hex literals: son los 6 puntos del espectro
     HSL puros (red→yellow→green→cyan→blue→magenta→red), no colores de UI. */
  background: linear-gradient(
    to right,
    #ff0000 0%,
    #ffff00 16.67%,
    #00ff00 33.33%,
    #00ffff 50%,
    #0000ff 66.67%,
    #ff00ff 83.33%,
    #ff0000 100%
  );
  overflow: hidden;
}

/* Hue slider handle/thumb */
.ren-color-picker-hue-handle {
  position: absolute;
  top: 50%;
  left: 0;
  width: 4px;
  height: 100%;
  background-color: white;
  box-shadow: 0 0 0 1px rgb(0, 0, 0, 0.3);
  pointer-events: none;
  transform: translateY(-50%);
}

/* ═══ ALPHA SLIDER (Optional) ═══ */
.ren-color-picker-alpha {
  position: relative;
  width: 100%;
  height: 32px;
  border-radius: var(--radius-md);
  border: 1px solid var(--color-border);
  cursor: pointer;
  overflow: hidden;

  /* Checkerboard pattern for transparency */
  background-image: repeating-conic-gradient(
    var(--color-border) 0deg 90deg,
    transparent 90deg 180deg
  );
  background-size: 8px 8px;
  background-position: 0 0;
}

/* Gradient overlay for alpha */
.ren-color-picker-alpha-gradient {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  border-radius: var(--radius-md);
}

/* Alpha slider handle/thumb */
.ren-color-picker-alpha-handle {
  position: absolute;
  top: 50%;
  left: 0;
  width: 4px;
  height: 100%;
  background-color: white;
  box-shadow: 0 0 0 1px rgb(0, 0, 0, 0.3);
  pointer-events: none;
  transform: translateY(-50%);
}

/* ═══ COLOR PREVIEW SWATCH ═══ */
.ren-color-picker-preview {
  display: flex;
  gap: var(--space-2);
  padding: var(--space-3);
  background-color: var(--color-surface);
  border-radius: var(--radius-md);
  border: 1px solid var(--color-border);
}

.ren-color-picker-preview-swatch {
  flex-shrink: 0;
  width: 48px;
  height: 48px;
  border-radius: var(--radius-md);
  border: 1px solid var(--color-border);

  /* Checkerboard for transparency */
  background-image: repeating-conic-gradient(
    var(--color-border) 0deg 90deg,
    transparent 90deg 180deg
  );
  background-size: 4px 4px;
  background-position: 0 0;
}

.ren-color-picker-preview-info {
  display: flex;
  flex-direction: column;
  justify-content: center;
  flex: 1;
}

.ren-color-picker-preview-label {
  font-size: var(--caption-size);
  color: var(--color-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  font-weight: var(--label-weight);
}

.ren-color-picker-preview-value {
  font-family: var(--font-mono);
  font-size: var(--label-size);
  color: var(--color-text);
  margin-top: var(--space-1);
}

/* ═══ TEXT INPUTS SECTION ═══ */
.ren-color-picker-inputs {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* Hex input field */
.ren-color-picker-hex-input {
  width: 100%;
  padding: var(--space-2) var(--space-3);
  font-size: var(--body-size);
  font-family: var(--font-mono);
  color: var(--color-text);
  background-color: var(--color-input-bg);
  border: 1px solid var(--color-input-border);
  border-radius: var(--radius-md);
  transition: var(--transition-tactile);
}

.ren-color-picker-hex-input:focus {
  outline: none;
  border-color: var(--color-input-border-focus);
  box-shadow: 0 0 0 3px var(--color-input-focus-ring);
}

.ren-color-picker-hex-input::placeholder {
  color: var(--color-text-muted);
}

/* Channel inputs container (RGB or HSL) */
.ren-color-picker-channels {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: var(--space-2);
}

/* Individual channel input */
.ren-color-picker-channel {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.ren-color-picker-channel-label {
  font-size: var(--caption-size);
  color: var(--color-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  font-weight: var(--label-weight);
}

.ren-color-picker-channel-input {
  padding: var(--space-2) var(--space-2);
  font-size: var(--body-size);
  font-family: var(--font-mono);
  color: var(--color-text);
  background-color: var(--color-input-bg);
  border: 1px solid var(--color-input-border);
  border-radius: var(--radius-md);
  text-align: center;
  transition: var(--transition-tactile);
}

.ren-color-picker-channel-input:focus {
  outline: none;
  border-color: var(--color-input-border-focus);
  box-shadow: 0 0 0 3px var(--color-input-focus-ring);
}

.ren-color-picker-channel-input::-webkit-outer-spin-button,
.ren-color-picker-channel-input::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

.ren-color-picker-channel-input[type="number"] {
  -moz-appearance: textfield;
}

/* ═══ FORMAT TOGGLE BUTTON ═══ */
.ren-color-picker-format-toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  font-size: var(--label-size);
  font-family: var(--font-mono);
  color: var(--color-text);
  background-color: var(--color-fill);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  cursor: pointer;
  white-space: nowrap;
  transition: var(--transition-tactile);
}

.ren-color-picker-format-toggle:hover {
  background-color: var(--color-fill-hover);
  border-color: var(--color-border);
}

.ren-color-picker-format-toggle:focus-visible {
  outline: none;
  border-color: var(--color-input-border-focus);
  box-shadow: 0 0 0 3px var(--color-input-focus-ring);
}

.ren-color-picker-format-toggle:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* ═══ EYEDROPPER BUTTON ═══ */
.ren-color-picker-eyedropper {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  font-size: var(--label-size);
  color: var(--color-text);
  background-color: var(--color-fill);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: var(--transition-tactile);
}

.ren-color-picker-eyedropper:hover {
  background-color: var(--color-fill-hover);
  border-color: var(--color-border);
}

.ren-color-picker-eyedropper:focus-visible {
  outline: none;
  border-color: var(--color-input-border-focus);
  box-shadow: 0 0 0 3px var(--color-input-focus-ring);
}

.ren-color-picker-eyedropper:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.ren-color-picker-eyedropper-icon {
  width: 16px;
  height: 16px;
  display: inline-block;
}

/* ═══ PRESETS/SWATCHES GRID ═══ */
.ren-color-picker-swatches {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(40px, 1fr));
  gap: var(--space-2);
  padding: var(--space-2);
  background-color: var(--color-surface);
  border-radius: var(--radius-md);
  border: 1px solid var(--color-border);
}

.ren-color-picker-swatch {
  position: relative;
  width: 40px;
  height: 40px;
  border-radius: var(--radius-md);
  border: 2px solid transparent;
  cursor: pointer;
  transition: var(--transition-tactile);
  background-image: repeating-conic-gradient(
    var(--color-border) 0deg 90deg,
    transparent 90deg 180deg
  );
  background-size: 4px 4px;
  background-position: 0 0;
}

.ren-color-picker-swatch:focus-visible {
  outline: none;
  border-color: var(--color-accent);
  box-shadow: 0 0 0 2px var(--color-input-focus-ring);
}

.ren-color-picker-swatch:hover {
  transform: scale(1.05);
  border-color: var(--color-border);
}

.ren-color-picker-swatch[aria-selected="true"] {
  border-color: var(--color-accent);
  box-shadow: 0 0 0 2px var(--color-surface), 0 0 0 4px var(--color-accent);
}

.ren-color-picker-swatch-color {
  position: absolute;
  inset: 0;
  border-radius: var(--radius-md);
}

/* ═══ CONTROLS ROW (Format + Eyedropper) ═══ */
.ren-color-picker-controls {
  display: flex;
  gap: var(--space-2);
}

.ren-color-picker-controls-left {
  flex: 1;
  display: flex;
  gap: var(--space-2);
}

.ren-color-picker-controls-right {
  display: flex;
  gap: var(--space-2);
}

/* ═══ DISABLED STATE ═══ */
.ren-color-picker[disabled],
.ren-color-picker-trigger[disabled] {
  opacity: 0.5;
  cursor: not-allowed;
  pointer-events: none;
}

/* ═══ ANIMATIONS & TRANSITIONS ═══ */
@media (prefers-reduced-motion: reduce) {
  .ren-color-picker-dropdown,
  .ren-color-picker-trigger,
  .ren-color-picker-hex-input,
  .ren-color-picker-channel-input,
  .ren-color-picker-format-toggle,
  .ren-color-picker-swatch {
    animation-duration: 0ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0ms !important;
  }
}
/* Appearance API bridge */
.ren-color-picker { inline-size: var(--ren-cp-width, 280px); border-radius: var(--ren-cp-radius, var(--radius-lg)); background: var(--ren-cp-bg, var(--color-surface-overlay)); box-shadow: var(--ren-cp-shadow, var(--shadow-lg)); }
.ren-color-picker .ren-color-picker-swatch { inline-size: var(--ren-cp-swatch-size, 24px); block-size: var(--ren-cp-swatch-size, 24px); }
.ren-color-picker .ren-color-picker-handle { inline-size: var(--ren-cp-handle-size, 16px); block-size: var(--ren-cp-handle-size, 16px); }
.ren-color-picker .ren-color-picker-track { block-size: var(--ren-cp-track-height, 12px); }


id: file:components/composites/ren-color-picker/ren-color-picker.js
type: javascript
name: ren-color-picker.js
path: components/composites/ren-color-picker/ren-color-picker.js
data: {}
body:
/**
 * RenDS — Color Picker Composite Component
 * ═════════════════════════════════════════════════════════════════════
 *
 * A fully featured, accessible color picker with support for:
 * - HSV/HSB internal color model with conversion to/from HEX, RGB, HSL
 * - Interactive saturation/brightness canvas area
 * - Hue slider with rainbow gradient
 * - Optional alpha/opacity slider
 * - Text inputs for manual color entry (HEX, RGB, HSL)
 * - Preset color swatches
 * - EyeDropper API for screen color picking (when available)
 * - Full keyboard navigation (arrow keys, Tab, Enter)
 * - ARIA labels and semantic markup
 * - Smooth Popover API animations
 *
 * Features:
 * - Light DOM, no Shadow DOM
 * - Uses design tokens for all UI chrome
 * - Canvas-based saturation area for accurate color rendering
 * - Real-time color conversion and validation
 * - Custom events: ren-change (committed), ren-input (live)
 * - Automatic sRGB color space handling
 *
 * @example
 * <ren-color-picker
 *   value="#3b82f6"
 *   alpha
 *   format="hex"
 *   swatches="#ef4444,#f97316,#eab308,#22c55e,#3b82f6,#8b5cf6,#ec4899">
 * </ren-color-picker>
 *
 * @fires ren-change - Dispatched when color is committed
 * @fires ren-input - Dispatched during interactive changes
 */

/* ═════════════════════════════════════════════════════════════════════
   COLOR CONVERSION UTILITIES
   ═════════════════════════════════════════════════════════════════════ */

/**
 * Convert RGB to HSV (hue, saturation, value/brightness)
 * Handles sRGB color space properly
 * Returns: { h: 0-360, s: 0-100, v: 0-100 }
 */
function rgbToHsv(r, g, b, a = 1) {
  r /= 255;
  g /= 255;
  b /= 255;

  const max = Math.max(r, g, b);
  const min = Math.min(r, g, b);
  const delta = max - min;
  let h = 0;
  let s = 0;
  let v = max;

  if (delta !== 0) {
    s = delta / max;

    if (max === r) {
      h = ((g - b) / delta + (g < b ? 6 : 0)) / 6;
    } else if (max === g) {
      h = ((b - r) / delta + 2) / 6;
    } else {
      h = ((r - g) / delta + 4) / 6;
    }
  }

  return {
    h: Math.round(h * 360),
    s: Math.round(s * 100),
    v: Math.round(v * 100),
    a,
  };
}

/**
 * Convert HSV to RGB
 * Inverse of rgbToHsv
 * Accepts: { h: 0-360, s: 0-100, v: 0-100, a: 0-1 }
 * Returns: { r: 0-255, g: 0-255, b: 0-255, a: 0-1 }
 */
function hsvToRgb(h, s, v, a = 1) {
  h = (h % 360) / 60;
  s = s / 100;
  v = v / 100;

  const c = v * s;
  const x = c * (1 - Math.abs((h % 2) - 1));
  const m = v - c;

  let r = 0,
    g = 0,
    b = 0;

  if (h >= 0 && h < 1) {
    r = c;
    g = x;
    b = 0;
  } else if (h >= 1 && h < 2) {
    r = x;
    g = c;
    b = 0;
  } else if (h >= 2 && h < 3) {
    r = 0;
    g = c;
    b = x;
  } else if (h >= 3 && h < 4) {
    r = 0;
    g = x;
    b = c;
  } else if (h >= 4 && h < 5) {
    r = x;
    g = 0;
    b = c;
  } else if (h >= 5 && h < 6) {
    r = c;
    g = 0;
    b = x;
  }

  return {
    r: Math.round((r + m) * 255),
    g: Math.round((g + m) * 255),
    b: Math.round((b + m) * 255),
    a,
  };
}

/**
 * Convert RGB to HEX string
 * Returns: "#RRGGBB" or "#RRGGBBAA" if alpha < 1
 */
function rgbToHex(r, g, b, a = 1) {
  const toHex = (n) => {
    const hex = Math.round(n).toString(16);
    return hex.length === 1 ? '0' + hex : hex;
  };

  const hex = `#${toHex(r)}${toHex(g)}${toHex(b)}`;

  if (a < 1) {
    return hex + toHex(a * 255);
  }
  return hex;
}

/**
 * Parse HEX color string to RGB values
 * Accepts: "#RGB", "#RRGGBB", "#RGBA", "#RRGGBBAA"
 * Returns: { r, g, b, a } or null if invalid
 */
function hexToRgb(hex) {
  hex = hex.replace(/^#/, '');

  // Handle shorthand
  if (hex.length === 3 || hex.length === 4) {
    hex = hex
      .split('')
      .map((c) => c + c)
      .join('');
  }

  if (hex.length !== 6 && hex.length !== 8) {
    return null;
  }

  const r = parseInt(hex.substring(0, 2), 16);
  const g = parseInt(hex.substring(2, 4), 16);
  const b = parseInt(hex.substring(4, 6), 16);
  const a = hex.length === 8 ? parseInt(hex.substring(6, 8), 16) / 255 : 1;

  if (isNaN(r) || isNaN(g) || isNaN(b)) {
    return null;
  }

  return { r, g, b, a };
}

/**
 * Convert RGB to HSL
 * Returns: { h: 0-360, s: 0-100, l: 0-100, a: 0-1 }
 */
function rgbToHsl(r, g, b, a = 1) {
  r /= 255;
  g /= 255;
  b /= 255;

  const max = Math.max(r, g, b);
  const min = Math.min(r, g, b);
  const delta = max - min;
  let h = 0;
  let s = 0;
  const l = (max + min) / 2;

  if (delta !== 0) {
    s = delta / (1 - Math.abs(2 * l - 1));

    if (max === r) {
      h = ((g - b) / delta + (g < b ? 6 : 0)) / 6;
    } else if (max === g) {
      h = ((b - r) / delta + 2) / 6;
    } else {
      h = ((r - g) / delta + 4) / 6;
    }
  }

  return {
    h: Math.round(h * 360),
    s: Math.round(s * 100),
    l: Math.round(l * 100),
    a,
  };
}

/**
 * Convert HSL to RGB
 * Accepts: { h: 0-360, s: 0-100, l: 0-100, a: 0-1 }
 * Returns: { r: 0-255, g: 0-255, b: 0-255, a: 0-1 }
 */
function hslToRgb(h, s, l, a = 1) {
  h = (h % 360) / 360;
  s = s / 100;
  l = l / 100;

  const hue2rgb = (p, q, t) => {
    if (t < 0) t += 1;
    if (t > 1) t -= 1;
    if (t < 1 / 6) return p + (q - p) * 6 * t;
    if (t < 1 / 2) return q;
    if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
    return p;
  };

  let r, g, b;
  if (s === 0) {
    r = g = b = l;
  } else {
    const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
    const p = 2 * l - q;
    r = hue2rgb(p, q, h + 1 / 3);
    g = hue2rgb(p, q, h);
    b = hue2rgb(p, q, h - 1 / 3);
  }

  return {
    r: Math.round(r * 255),
    g: Math.round(g * 255),
    b: Math.round(b * 255),
    a,
  };
}

/**
 * Validate and normalize hex color string
 */
function normalizeHex(hex) {
  if (typeof hex !== 'string') return null;
  hex = hex.trim();
  if (!hex.startsWith('#')) hex = '#' + hex;
  const result = hexToRgb(hex);
  return result ? rgbToHex(result.r, result.g, result.b, result.a) : null;
}

const COLOR_PICKER_SIDES = new Set(['top', 'right', 'bottom', 'left']);

function normalizeColorPickerSide(value) {
  const side = String(value || 'bottom').toLowerCase().split('-')[0];

  return COLOR_PICKER_SIDES.has(side) ? side : 'bottom';
}

/* ═════════════════════════════════════════════════════════════════════
   COLOR PICKER WEB COMPONENT
   ═════════════════════════════════════════════════════════════════════ */

export class RenColorPicker extends HTMLElement {
  static observedAttributes = ['placement'];

  #isOpen = false;
  #hsv = { h: 0, s: 100, v: 100, a: 1 };
  #format = 'hex'; // 'hex', 'rgb', 'hsl'
  #saturationCanvas = null;
  #dragState = null;
  #popover = null;
  #trigger = null;
  #isDragging = false;
  #listenerController = null;

  constructor() {
    super();
    this.handleTriggerClick = this.handleTriggerClick.bind(this);
    this.handleSaturationMouseDown = this.handleSaturationMouseDown.bind(this);
    this.handleHueMouseDown = this.handleHueMouseDown.bind(this);
    this.handleAlphaMouseDown = this.handleAlphaMouseDown.bind(this);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.handleMouseUp = this.handleMouseUp.bind(this);
    this.handleInputChange = this.handleInputChange.bind(this);
    this.handleFormatToggle = this.handleFormatToggle.bind(this);
    this.handleSwatchClick = this.handleSwatchClick.bind(this);
    this.handleEyedropper = this.handleEyedropper.bind(this);
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'placement' && oldValue !== newValue) {
      this.#syncPlacement();
    }
  }

  connectedCallback() {
    this.#listenerController?.abort();
    this.#listenerController = new AbortController();
    const { signal } = this.#listenerController;

    // ═══ PARSE ATTRIBUTES ═══
    const value = this.getAttribute('value') || '#3b82f6';
    const hasAlpha = this.hasAttribute('alpha');
    const format = this.getAttribute('format') || 'hex';
    const swatches = this.getAttribute('swatches') || '';
    const disabled = this.hasAttribute('disabled');

    this.#format = ['hex', 'rgb', 'hsl'].includes(format) ? format : 'hex';

    // ═══ PARSE INITIAL COLOR ═══
    this.setValue(value);

    // ═══ RENDER TRIGGER ═══
    this.#renderTrigger(disabled);

    // ═══ SET UP EVENT LISTENERS ═══
    if (this.#trigger) {
      this.#trigger.addEventListener('click', this.handleTriggerClick, { signal });
      this.#trigger.addEventListener('keydown', (e) => {
        if (e.key === 'Enter' || e.key === ' ') {
          this.handleTriggerClick();
          e.preventDefault();
        }
      }, { signal });
    }

    // ═══ RENDER DROPDOWN (will be populated on open) ═══
    this.#popover = this.querySelector(':scope > .ren-color-picker-dropdown');
    if (!this.#popover) {
      this.#popover = document.createElement('div');
      this.#popover.className = 'ren-color-picker-dropdown';
      this.appendChild(this.#popover);
    }
    const popoverId = this.#popover.id || `ren-color-picker-${Math.random().toString(36).slice(2)}`;
    this.#popover.id = popoverId;
    this.#popover.setAttribute('popover', 'auto');

    if (this.#trigger) {
      this.#trigger.setAttribute('aria-haspopup', 'dialog');
      this.#trigger.setAttribute('aria-expanded', 'false');
      this.#trigger.setAttribute('popovertarget', popoverId);
    }

    this.#syncPlacement();

    // ═══ POPULATE POPOVER ON OPEN ═══
    this.#popover.addEventListener('toggle', (e) => {
      if (e.newState === 'open') {
        this.#isOpen = true;
        if (this.#trigger) {
          this.#trigger.setAttribute('aria-expanded', 'true');
        }
        this.#renderDropdownContent(hasAlpha, swatches, disabled);
      } else {
        this.#isOpen = false;
        if (this.#trigger) {
          this.#trigger.setAttribute('aria-expanded', 'false');
        }
      }
    }, { signal });
  }

  disconnectedCallback() {
    this.#listenerController?.abort();
    this.#listenerController = null;
    document.removeEventListener('mousemove', this.handleMouseMove);
    document.removeEventListener('mouseup', this.handleMouseUp);
  }

  /* ═════════════════════════════════════════════════════════════════
     RENDERING METHODS
     ═════════════════════════════════════════════════════════════════ */

  #syncPlacement() {
    const side = normalizeColorPickerSide(this.getAttribute('placement'));

    this.setAttribute('data-side', side);
    if (this.#popover) {
      this.#popover.setAttribute('data-side', side);
    }
  }

  /**
   * Render the trigger button showing the selected color
   */
  #renderTrigger(disabled = false) {
    // Remove existing trigger
    const existing = this.querySelector('.ren-color-picker-trigger');
    if (existing) existing.remove();

    const trigger = document.createElement('button');
    trigger.className = 'ren-color-picker-trigger';
    trigger.type = 'button';
    if (disabled) trigger.disabled = true;

    const rgb = hsvToRgb(this.#hsv.h, this.#hsv.s, this.#hsv.v, this.#hsv.a);
    const hex = rgbToHex(rgb.r, rgb.g, rgb.b, rgb.a);

    trigger.innerHTML = `
      <div class="ren-color-picker-trigger-swatch" style="background-color: ${this.#hsvToCss()}"></div>
      <div class="ren-color-picker-trigger-value">${hex}</div>
    `;

    trigger.setAttribute('aria-label', `Color picker, current color ${hex}`);

    this.insertBefore(trigger, this.#popover);
    this.#trigger = trigger;
  }

  /**
   * Render the full dropdown content (called when popover opens)
   */
  #renderDropdownContent(hasAlpha = false, swatches = '', disabled = false) {
    if (!this.#popover) return;

    // Clear existing content
    this.#popover.innerHTML = '';

    // ═══ SATURATION/BRIGHTNESS AREA ═══
    const saturationContainer = document.createElement('div');
    saturationContainer.className = 'ren-color-picker-saturation';
    saturationContainer.setAttribute('aria-label', 'Color saturation and brightness');

    this.#saturationCanvas = document.createElement('canvas');
    this.#saturationCanvas.className = 'ren-color-picker-saturation-canvas';
    this.#saturationCanvas.width = 240;
    this.#saturationCanvas.height = 240;

    saturationContainer.appendChild(this.#saturationCanvas);

    const handle = document.createElement('div');
    handle.className = 'ren-color-picker-saturation-handle';
    saturationContainer.appendChild(handle);

    this.#drawSaturationCanvas();
    this.#updateSaturationHandle();

    saturationContainer.addEventListener('mousedown', this.handleSaturationMouseDown);
    this.#popover.appendChild(saturationContainer);

    // ═══ HUE SLIDER ═══
    const hueContainer = document.createElement('div');
    hueContainer.className = 'ren-color-picker-hue';
    hueContainer.setAttribute('aria-label', 'Color hue');
    hueContainer.setAttribute('role', 'slider');
    hueContainer.setAttribute('aria-valuenow', this.#hsv.h);
    hueContainer.setAttribute('aria-valuemin', '0');
    hueContainer.setAttribute('aria-valuemax', '360');
    hueContainer.setAttribute('aria-orientation', 'horizontal');
    hueContainer.setAttribute('tabindex', '0');

    const hueHandle = document.createElement('div');
    hueHandle.className = 'ren-color-picker-hue-handle';
    hueContainer.appendChild(hueHandle);

    this.#updateHueHandle();

    hueContainer.addEventListener('mousedown', this.handleHueMouseDown);
    hueContainer.addEventListener('keydown', (e) => {
      this.#handleHueKeydown(e, hueContainer);
    });

    this.#popover.appendChild(hueContainer);

    // ═══ ALPHA SLIDER (Optional) ═══
    if (hasAlpha) {
      const alphaContainer = document.createElement('div');
      alphaContainer.className = 'ren-color-picker-alpha';
      alphaContainer.setAttribute('aria-label', 'Color opacity');
      alphaContainer.setAttribute('role', 'slider');
      alphaContainer.setAttribute('aria-valuenow', Math.round(this.#hsv.a * 100));
      alphaContainer.setAttribute('aria-valuemin', '0');
      alphaContainer.setAttribute('aria-valuemax', '100');
      alphaContainer.setAttribute('aria-orientation', 'horizontal');
      alphaContainer.setAttribute('tabindex', '0');

      const alphaGradient = document.createElement('div');
      alphaGradient.className = 'ren-color-picker-alpha-gradient';
      alphaContainer.appendChild(alphaGradient);

      const alphaHandle = document.createElement('div');
      alphaHandle.className = 'ren-color-picker-alpha-handle';
      alphaContainer.appendChild(alphaHandle);

      this.#updateAlphaGradient();
      this.#updateAlphaHandle();

      alphaContainer.addEventListener('mousedown', this.handleAlphaMouseDown);
      alphaContainer.addEventListener('keydown', (e) => {
        this.#handleAlphaKeydown(e, alphaContainer);
      });

      this.#popover.appendChild(alphaContainer);
    }

    // ═══ COLOR PREVIEW ═══
    const preview = document.createElement('div');
    preview.className = 'ren-color-picker-preview';

    const swatch = document.createElement('div');
    swatch.className = 'ren-color-picker-preview-swatch';
    swatch.style.backgroundColor = this.#hsvToCss();

    const info = document.createElement('div');
    info.className = 'ren-color-picker-preview-info';

    const label = document.createElement('div');
    label.className = 'ren-color-picker-preview-label';
    label.textContent = 'Current';

    const value = document.createElement('div');
    value.className = 'ren-color-picker-preview-value';
    const rgb = hsvToRgb(this.#hsv.h, this.#hsv.s, this.#hsv.v, this.#hsv.a);
    value.textContent = rgbToHex(rgb.r, rgb.g, rgb.b, rgb.a);

    info.appendChild(label);
    info.appendChild(value);
    preview.appendChild(swatch);
    preview.appendChild(info);
    this.#popover.appendChild(preview);

    // ═══ TEXT INPUTS SECTION ═══
    const inputsSection = document.createElement('div');
    inputsSection.className = 'ren-color-picker-inputs';

    // Hex input
    const hexInput = document.createElement('input');
    hexInput.className = 'ren-color-picker-hex-input';
    hexInput.type = 'text';
    hexInput.placeholder = '#000000';
    hexInput.value = rgbToHex(rgb.r, rgb.g, rgb.b, rgb.a);
    hexInput.setAttribute('aria-label', 'Hex color value');
    hexInput.disabled = disabled;
    hexInput.addEventListener('change', this.handleInputChange);
    inputsSection.appendChild(hexInput);

    // RGB or HSL channels
    const channelsContainer = document.createElement('div');
    channelsContainer.className = 'ren-color-picker-channels';

    let channels = [];
    if (this.#format === 'rgb') {
      channels = [
        { label: 'R', value: rgb.r, min: 0, max: 255, key: 'r' },
        { label: 'G', value: rgb.g, min: 0, max: 255, key: 'g' },
        { label: 'B', value: rgb.b, min: 0, max: 255, key: 'b' },
      ];
    } else if (this.#format === 'hsl') {
      const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b, rgb.a);
      channels = [
        { label: 'H', value: hsl.h, min: 0, max: 360, key: 'h' },
        { label: 'S', value: hsl.s, min: 0, max: 100, key: 's' },
        { label: 'L', value: hsl.l, min: 0, max: 100, key: 'l' },
      ];
    } else {
      channels = [
        { label: 'R', value: rgb.r, min: 0, max: 255, key: 'r' },
        { label: 'G', value: rgb.g, min: 0, max: 255, key: 'g' },
        { label: 'B', value: rgb.b, min: 0, max: 255, key: 'b' },
      ];
    }

    channels.forEach(({ label, value, min, max, key }) => {
      const channel = document.createElement('div');
      channel.className = 'ren-color-picker-channel';

      const channelLabel = document.createElement('label');
      channelLabel.className = 'ren-color-picker-channel-label';
      channelLabel.textContent = label;

      const channelInput = document.createElement('input');
      channelInput.className = 'ren-color-picker-channel-input';
      channelInput.type = 'number';
      channelInput.min = min;
      channelInput.max = max;
      channelInput.value = value;
      channelInput.disabled = disabled;
      channelInput.setAttribute('aria-label', `${label} channel`);
      channelInput.addEventListener('change', this.handleInputChange);

      channel.appendChild(channelLabel);
      channel.appendChild(channelInput);
      channelsContainer.appendChild(channel);
    });

    inputsSection.appendChild(channelsContainer);
    this.#popover.appendChild(inputsSection);

    // ═══ CONTROLS (Format Toggle + Eyedropper) ═══
    const controls = document.createElement('div');
    controls.className = 'ren-color-picker-controls';

    const controlsLeft = document.createElement('div');
    controlsLeft.className = 'ren-color-picker-controls-left';

    const formatToggle = document.createElement('button');
    formatToggle.className = 'ren-color-picker-format-toggle';
    formatToggle.type = 'button';
    formatToggle.textContent = this.#format.toUpperCase();
    formatToggle.setAttribute('aria-label', 'Toggle color format between HEX, RGB, and HSL');
    formatToggle.disabled = disabled;
    formatToggle.addEventListener('click', this.handleFormatToggle);

    controlsLeft.appendChild(formatToggle);

    // Eyedropper (if available)
    if ('EyeDropper' in window) {
      const eyedropperBtn = document.createElement('button');
      eyedropperBtn.className = 'ren-color-picker-eyedropper';
      eyedropperBtn.type = 'button';
      eyedropperBtn.setAttribute('aria-label', 'Pick color from screen');
      eyedropperBtn.disabled = disabled;

      // Simple eyedropper icon (SVG)
      const icon = document.createElement('span');
      icon.className = 'ren-color-picker-eyedropper-icon';
      icon.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2L22 12L12 22L2 12Z"></path></svg>`;

      eyedropperBtn.appendChild(icon);
      eyedropperBtn.addEventListener('click', this.handleEyedropper);
      controlsLeft.appendChild(eyedropperBtn);
    }

    controls.appendChild(controlsLeft);
    this.#popover.appendChild(controls);

    // ═══ PRESETS/SWATCHES (Optional) ═══
    if (swatches.trim()) {
      const swatchesContainer = document.createElement('div');
      swatchesContainer.className = 'ren-color-picker-swatches';
      swatchesContainer.setAttribute('aria-label', 'Preset color swatches');

      const colors = swatches.split(',').map((s) => s.trim());
      colors.forEach((color) => {
        const swatch = document.createElement('button');
        swatch.className = 'ren-color-picker-swatch';
        swatch.type = 'button';
        swatch.setAttribute('aria-label', `Color ${color}`);

        const colorDiv = document.createElement('div');
        colorDiv.className = 'ren-color-picker-swatch-color';
        colorDiv.style.backgroundColor = color;

        swatch.appendChild(colorDiv);
        swatch.addEventListener('click', this.handleSwatchClick);

        // Check if this is the current color
        const normalized = normalizeHex(color);
        const currentHex = rgbToHex(rgb.r, rgb.g, rgb.b, rgb.a);
        if (normalized === currentHex) {
          swatch.setAttribute('aria-selected', 'true');
        }

        swatchesContainer.appendChild(swatch);
      });

      this.#popover.appendChild(swatchesContainer);
    }
  }

  /* ═════════════════════════════════════════════════════════════════
     CANVAS & HANDLE UPDATES
     ═════════════════════════════════════════════════════════════════ */

  /**
   * Draw the saturation/brightness canvas
   */
  #drawSaturationCanvas() {
    if (!this.#saturationCanvas) return;

    const ctx = this.#saturationCanvas.getContext('2d');
    const { width, height } = this.#saturationCanvas;

    // Create horizontal gradient: white (left) to hue (right)
    const rgb = hsvToRgb(this.#hsv.h, 100, 100, 1);
    const hueColor = `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`;

    const hGradient = ctx.createLinearGradient(0, 0, width, 0);
    hGradient.addColorStop(0, 'white');
    hGradient.addColorStop(1, hueColor);

    ctx.fillStyle = hGradient;
    ctx.fillRect(0, 0, width, height);

    // Create vertical gradient overlay: transparent (top) to black (bottom)
    const vGradient = ctx.createLinearGradient(0, 0, 0, height);
    vGradient.addColorStop(0, 'rgba(0, 0, 0, 0)');
    vGradient.addColorStop(1, 'rgba(0, 0, 0, 1)');

    ctx.fillStyle = vGradient;
    ctx.fillRect(0, 0, width, height);
  }

  /**
   * Update saturation handle position
   */
  #updateSaturationHandle() {
    if (!this.#saturationCanvas) return;

    const handle = this.#saturationCanvas.parentElement.querySelector(
      '.ren-color-picker-saturation-handle'
    );
    if (!handle) return;

    const { width, height } = this.#saturationCanvas;
    const x = (this.#hsv.s / 100) * width;
    const y = ((100 - this.#hsv.v) / 100) * height;

    handle.style.left = x + 'px';
    handle.style.top = y + 'px';
  }

  /**
   * Update hue handle position
   */
  #updateHueHandle() {
    const hueContainer = this.#popover?.querySelector('.ren-color-picker-hue');
    if (!hueContainer) return;

    const handle = hueContainer.querySelector('.ren-color-picker-hue-handle');
    if (!handle) return;

    const percent = (this.#hsv.h / 360) * 100;
    handle.style.left = percent + '%';

    hueContainer.setAttribute('aria-valuenow', this.#hsv.h);
  }

  /**
   * Update alpha gradient and handle
   */
  #updateAlphaGradient() {
    const alphaContainer = this.#popover?.querySelector('.ren-color-picker-alpha');
    if (!alphaContainer) return;

    const gradient = alphaContainer.querySelector('.ren-color-picker-alpha-gradient');
    if (!gradient) return;

    const rgb = hsvToRgb(this.#hsv.h, this.#hsv.s, this.#hsv.v, 1);
    const color = `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`;

    gradient.style.background = `linear-gradient(to right, rgba(0,0,0,0), ${color})`;
  }

  /**
   * Update alpha handle position
   */
  #updateAlphaHandle() {
    const alphaContainer = this.#popover?.querySelector('.ren-color-picker-alpha');
    if (!alphaContainer) return;

    const handle = alphaContainer.querySelector('.ren-color-picker-alpha-handle');
    if (!handle) return;

    const percent = this.#hsv.a * 100;
    handle.style.left = percent + '%';

    alphaContainer.setAttribute('aria-valuenow', Math.round(percent));
  }

  /* ═════════════════════════════════════════════════════════════════
     EVENT HANDLERS
     ═════════════════════════════════════════════════════════════════ */

  handleTriggerClick(event) {
    event?.preventDefault();
    if (!this.#popover) return;

    try {
      if (this.#popover.matches(':popover-open')) {
        this.#popover.hidePopover?.();
      } else {
        this.#popover.showPopover?.();
      }
    } catch {
      // Native popover can throw if browser state changes mid-click.
    }
  }

  handleSaturationMouseDown(e) {
    e.preventDefault();
    this.#isDragging = true;
    this.#dragState = { type: 'saturation' };

    this.#updateSaturationFromMouse(e);

    document.addEventListener('mousemove', this.handleMouseMove);
    document.addEventListener('mouseup', this.handleMouseUp);
  }

  handleHueMouseDown(e) {
    e.preventDefault();
    this.#isDragging = true;
    this.#dragState = { type: 'hue' };

    this.#updateHueFromMouse(e);

    document.addEventListener('mousemove', this.handleMouseMove);
    document.addEventListener('mouseup', this.handleMouseUp);
  }

  handleAlphaMouseDown(e) {
    e.preventDefault();
    this.#isDragging = true;
    this.#dragState = { type: 'alpha' };

    this.#updateAlphaFromMouse(e);

    document.addEventListener('mousemove', this.handleMouseMove);
    document.addEventListener('mouseup', this.handleMouseUp);
  }

  handleMouseMove(e) {
    if (!this.#isDragging || !this.#dragState) return;

    if (this.#dragState.type === 'saturation') {
      this.#updateSaturationFromMouse(e);
    } else if (this.#dragState.type === 'hue') {
      this.#updateHueFromMouse(e);
    } else if (this.#dragState.type === 'alpha') {
      this.#updateAlphaFromMouse(e);
    }
  }

  handleMouseUp(e) {
    this.#isDragging = false;
    this.#dragState = null;

    document.removeEventListener('mousemove', this.handleMouseMove);
    document.removeEventListener('mouseup', this.handleMouseUp);

    this.#emitChange();
  }

  handleInputChange(e) {
    const input = e.target;
    const value = input.value.trim();

    if (input.classList.contains('ren-color-picker-hex-input')) {
      const rgb = hexToRgb(value);
      if (rgb) {
        const hsv = rgbToHsv(rgb.r, rgb.g, rgb.b, rgb.a);
        this.#hsv = hsv;
        this.#updateAllViews();
        this.#emitChange();
      }
    } else if (input.classList.contains('ren-color-picker-channel-input')) {
      this.#updateFromChannelInputs();
      this.#emitChange();
    }
  }

  handleFormatToggle() {
    const formats = ['hex', 'rgb', 'hsl'];
    const currentIndex = formats.indexOf(this.#format);
    this.#format = formats[(currentIndex + 1) % formats.length];

    // Re-render inputs
    this.#renderDropdownContent(
      this.hasAttribute('alpha'),
      this.getAttribute('swatches') || '',
      this.hasAttribute('disabled')
    );
  }

  handleSwatchClick(e) {
    const swatch = e.currentTarget;
    const colorDiv = swatch.querySelector('.ren-color-picker-swatch-color');
    const color = colorDiv.style.backgroundColor;

    this.setValue(color);
    this.#updateAllViews();
    this.#emitChange();
  }

  async handleEyedropper() {
    try {
      const eyeDropper = new EyeDropper();
      const result = await eyeDropper.open();
      this.setValue(result.sRGBHex);
      this.#updateAllViews();
      this.#emitChange();
    } catch {
      // User cancelled or not supported
    }
  }

  /* ═════════════════════════════════════════════════════════════════
     KEYBOARD NAVIGATION
     ═════════════════════════════════════════════════════════════════ */

  #handleHueKeydown(e, hueContainer) {
    const step = e.shiftKey ? 10 : 1;

    if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
      this.#hsv.h = (this.#hsv.h + step) % 360;
      this.#updateAllViews();
      this.#emitInput();
      e.preventDefault();
    } else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
      this.#hsv.h = (this.#hsv.h - step + 360) % 360;
      this.#updateAllViews();
      this.#emitInput();
      e.preventDefault();
    }
  }

  #handleAlphaKeydown(e, alphaContainer) {
    const step = e.shiftKey ? 0.1 : 0.01;

    if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
      this.#hsv.a = Math.min(1, this.#hsv.a + step);
      this.#updateAllViews();
      this.#emitInput();
      e.preventDefault();
    } else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
      this.#hsv.a = Math.max(0, this.#hsv.a - step);
      this.#updateAllViews();
      this.#emitInput();
      e.preventDefault();
    }
  }

  /* ═════════════════════════════════════════════════════════════════
     INTERNAL UPDATES
     ═════════════════════════════════════════════════════════════════ */

  #updateSaturationFromMouse(e) {
    if (!this.#saturationCanvas) return;

    const rect = this.#saturationCanvas.getBoundingClientRect();
    const x = Math.max(0, Math.min(rect.width, e.clientX - rect.left));
    const y = Math.max(0, Math.min(rect.height, e.clientY - rect.top));

    this.#hsv.s = Math.round((x / rect.width) * 100);
    this.#hsv.v = Math.round(100 - (y / rect.height) * 100);

    this.#updateSaturationHandle();
    this.#updateAlphaGradient();
    this.#updatePreview();
    this.#emitInput();
  }

  #updateHueFromMouse(e) {
    const hueContainer = this.#popover?.querySelector('.ren-color-picker-hue');
    if (!hueContainer) return;

    const rect = hueContainer.getBoundingClientRect();
    const x = Math.max(0, Math.min(rect.width, e.clientX - rect.left));
    this.#hsv.h = Math.round((x / rect.width) * 360) % 360;

    this.#drawSaturationCanvas();
    this.#updateSaturationHandle();
    this.#updateHueHandle();
    this.#updateAlphaGradient();
    this.#updatePreview();
    this.#updateChannelInputs();
    this.#emitInput();
  }

  #updateAlphaFromMouse(e) {
    const alphaContainer = this.#popover?.querySelector('.ren-color-picker-alpha');
    if (!alphaContainer) return;

    const rect = alphaContainer.getBoundingClientRect();
    const x = Math.max(0, Math.min(rect.width, e.clientX - rect.left));
    this.#hsv.a = Math.round((x / rect.width) * 100) / 100;

    this.#updateAlphaHandle();
    this.#updatePreview();
    this.#emitInput();
  }

  #updateFromChannelInputs() {
    const inputs = this.#popover?.querySelectorAll('.ren-color-picker-channel-input');
    if (!inputs || inputs.length === 0) return;

    const values = Array.from(inputs).map((inp) => parseFloat(inp.value) || 0);

    if (this.#format === 'rgb') {
      const [r, g, b] = values;
      const hsv = rgbToHsv(r, g, b, this.#hsv.a);
      this.#hsv = hsv;
    } else if (this.#format === 'hsl') {
      const [h, s, l] = values;
      const rgb = hslToRgb(h, s, l, this.#hsv.a);
      const hsv = rgbToHsv(rgb.r, rgb.g, rgb.b, rgb.a);
      this.#hsv = hsv;
    } else {
      const [r, g, b] = values;
      const hsv = rgbToHsv(r, g, b, this.#hsv.a);
      this.#hsv = hsv;
    }

    this.#updateAllViews();
  }

  #updateAllViews() {
    this.#drawSaturationCanvas();
    this.#updateSaturationHandle();
    this.#updateHueHandle();
    this.#updateAlphaGradient();
    this.#updateAlphaHandle();
    this.#updatePreview();
    this.#updateChannelInputs();
    this.#updateHexInput();
    this.#updateTrigger();
  }

  #updatePreview() {
    const preview = this.#popover?.querySelector('.ren-color-picker-preview-swatch');
    const value = this.#popover?.querySelector('.ren-color-picker-preview-value');

    if (preview) {
      preview.style.backgroundColor = this.#hsvToCss();
    }

    if (value) {
      const rgb = hsvToRgb(this.#hsv.h, this.#hsv.s, this.#hsv.v, this.#hsv.a);
      value.textContent = rgbToHex(rgb.r, rgb.g, rgb.b, rgb.a);
    }
  }

  #updateHexInput() {
    const input = this.#popover?.querySelector('.ren-color-picker-hex-input');
    if (!input) return;

    const rgb = hsvToRgb(this.#hsv.h, this.#hsv.s, this.#hsv.v, this.#hsv.a);
    input.value = rgbToHex(rgb.r, rgb.g, rgb.b, rgb.a);
  }

  #updateChannelInputs() {
    const inputs = this.#popover?.querySelectorAll('.ren-color-picker-channel-input');
    if (!inputs || inputs.length === 0) return;

    const rgb = hsvToRgb(this.#hsv.h, this.#hsv.s, this.#hsv.v, this.#hsv.a);

    if (this.#format === 'rgb') {
      const values = [rgb.r, rgb.g, rgb.b];
      Array.from(inputs).forEach((inp, i) => {
        inp.value = values[i];
      });
    } else if (this.#format === 'hsl') {
      const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b, rgb.a);
      const values = [hsl.h, hsl.s, hsl.l];
      Array.from(inputs).forEach((inp, i) => {
        inp.value = values[i];
      });
    } else {
      const values = [rgb.r, rgb.g, rgb.b];
      Array.from(inputs).forEach((inp, i) => {
        inp.value = values[i];
      });
    }
  }

  #updateTrigger() {
    if (!this.#trigger) return;

    const swatch = this.#trigger.querySelector('.ren-color-picker-trigger-swatch');
    const value = this.#trigger.querySelector('.ren-color-picker-trigger-value');

    if (swatch) {
      swatch.style.backgroundColor = this.#hsvToCss();
    }

    if (value) {
      const rgb = hsvToRgb(this.#hsv.h, this.#hsv.s, this.#hsv.v, this.#hsv.a);
      value.textContent = rgbToHex(rgb.r, rgb.g, rgb.b, rgb.a);
    }
  }

  #hsvToCss() {
    const rgb = hsvToRgb(this.#hsv.h, this.#hsv.s, this.#hsv.v, this.#hsv.a);
    if (this.#hsv.a < 1) {
      return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${rgb.a})`;
    }
    return `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`;
  }

  /* ═════════════════════════════════════════════════════════════════
     EVENT DISPATCHERS
     ═════════════════════════════════════════════════════════════════ */

  #emitInput() {
    const rgb = hsvToRgb(this.#hsv.h, this.#hsv.s, this.#hsv.v, this.#hsv.a);
    const hex = rgbToHex(rgb.r, rgb.g, rgb.b, rgb.a);
    const rgbStr = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${rgb.a})`;
    const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b, rgb.a);
    const hslStr = `hsla(${hsl.h}, ${hsl.s}%, ${hsl.l}%, ${hsl.a})`;

    this.dispatchEvent(
      new CustomEvent('ren-input', {
        detail: { hex, rgb: rgbStr, hsl: hslStr, alpha: rgb.a },
        bubbles: true,
        composed: true,
      })
    );
  }

  #emitChange() {
    const rgb = hsvToRgb(this.#hsv.h, this.#hsv.s, this.#hsv.v, this.#hsv.a);
    const hex = rgbToHex(rgb.r, rgb.g, rgb.b, rgb.a);
    const rgbStr = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${rgb.a})`;
    const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b, rgb.a);
    const hslStr = `hsla(${hsl.h}, ${hsl.s}%, ${hsl.l}%, ${hsl.a})`;

    this.dispatchEvent(
      new CustomEvent('ren-change', {
        detail: { hex, rgb: rgbStr, hsl: hslStr, alpha: rgb.a },
        bubbles: true,
        composed: true,
      })
    );
  }

  /* ═════════════════════════════════════════════════════════════════
     PUBLIC API
     ═════════════════════════════════════════════════════════════════ */

  /**
   * Set the color value
   * Accepts: hex string, rgb/rgba string, hsl/hsla string, or color name
   */
  setValue(color) {
    let rgb = null;

    if (typeof color === 'string') {
      // Try to parse as hex
      rgb = hexToRgb(color);

      // If not hex, assume it's rgb/hsl/color name
      if (!rgb) {
        // Use temporary element to parse CSS color
        const div = document.createElement('div');
        div.style.color = color;
        document.body.appendChild(div);
        const computed = getComputedStyle(div).color;
        document.body.removeChild(div);

        // Parse rgb() string
        const match = computed.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
        if (match) {
          rgb = {
            r: parseInt(match[1]),
            g: parseInt(match[2]),
            b: parseInt(match[3]),
            a: match[4] ? parseFloat(match[4]) : 1,
          };
        }
      }
    }

    if (rgb) {
      this.#hsv = rgbToHsv(rgb.r, rgb.g, rgb.b, rgb.a);
    }
  }

  /**
   * Get the current color value
   * Returns: { hex, rgb, hsl, alpha }
   */
  getValue() {
    const rgb = hsvToRgb(this.#hsv.h, this.#hsv.s, this.#hsv.v, this.#hsv.a);
    const hex = rgbToHex(rgb.r, rgb.g, rgb.b, rgb.a);
    const rgbStr = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${rgb.a})`;
    const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b, rgb.a);
    const hslStr = `hsla(${hsl.h}, ${hsl.s}%, ${hsl.l}%, ${hsl.a})`;

    return {
      hex,
      rgb: rgbStr,
      hsl: hslStr,
      alpha: rgb.a,
    };
  }
}

customElements.define('ren-color-picker', RenColorPicker);


id: file:components/composites/ren-combobox/ren-combobox.css
type: css
name: ren-combobox.css
path: components/composites/ren-combobox/ren-combobox.css
data: {}
body:
/**
 * RenDS — Combobox
 * =================
 * Light-DOM combobox. CSS works standalone; <ren-combobox> layers ARIA
 * and behavior on top.
 *
 * Markup:
 *   <div class="ren-combobox">
 *     <input class="ren-combobox-input">
 *     <div class="ren-combobox-list" role="listbox" hidden>
 *       <div class="ren-combobox-item" role="option">…</div>
 *     </div>
 *   </div>
 */

.ren-combobox {
  position: relative;
  display: block;
  width: 100%;

  &-input {
    width: 100%;
    padding-block: var(--space-2);
    padding-inline: var(--space-3);

    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    background: var(--color-surface);

    font-family: inherit;
    font-size: var(--text-sm);
    line-height: var(--leading-5);
    color: var(--color-text);

    transition: var(--transition-tactile);

    &::placeholder {
      color: var(--color-text-muted);
    }

    &:focus {
      outline: none;
      border-color: var(--color-accent);
      box-shadow: 0 0 0 3px var(--color-accent-subtle);
    }

    &:disabled {
      background: var(--color-fill);
      color: var(--color-text-muted);
      cursor: not-allowed;
    }

    /* Hide native type="search" decorations */
    &::-webkit-search-cancel-button,
    &::-webkit-search-decoration {
      -webkit-appearance: none;
      appearance: none;
      display: none;
    }
  }

  &-list {
    position: absolute;
    inset-inline: 0;
    top: calc(100% + var(--space-1));
    z-index: 50;

    max-height: 15rem;
    padding-block: var(--space-1);

    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg);

    overflow-y: auto;
    overflow-x: hidden;

    /* Scrollbar styling */
    scrollbar-width: thin;
    scrollbar-color: var(--color-fill-active) transparent;

    &::-webkit-scrollbar {
      width: 6px;
    }

    &::-webkit-scrollbar-track {
      background: transparent;
    }

    &::-webkit-scrollbar-thumb {
      background: var(--color-fill-active);
      border-radius: var(--radius-full);
    }

    &::-webkit-scrollbar-thumb:hover {
      background: var(--color-fill-hover);
    }

    &[hidden] {
      display: none;
    }

    /* Open animation */
    @starting-style {
      opacity: 0;
      translate: 0 -4px;
    }
    animation: ren-combobox-open var(--duration-enter) var(--ease-enter);

    @media (prefers-reduced-motion: reduce) {
      animation: none;
    }
  }

  &[data-side="top"] &-list,
  &-list[data-side="top"] {
    top: auto;
    bottom: calc(100% + var(--space-1));
  }

  &[data-side="bottom"] &-list,
  &-list[data-side="bottom"] {
    top: calc(100% + var(--space-1));
    bottom: auto;
  }

  &-item {
    padding: var(--space-2) var(--space-3);
    cursor: pointer;
    border-radius: var(--radius-sm);

    display: flex;
    flex-direction: column;
    gap: var(--space-0-5, 2px);

    font-size: var(--text-sm);
    color: var(--color-text);

    transition: background-color var(--duration-micro) var(--ease-enter);

    &[hidden] {
      display: none;
    }

    &[data-highlighted] {
      background: var(--color-fill);
    }

    &[aria-selected="true"] {
      background: var(--color-accent-subtle);
      color: var(--color-accent);
      font-weight: 500;
    }

    &[aria-disabled="true"] {
      opacity: 0.5;
      cursor: not-allowed;

      &:hover,
      &[data-highlighted] {
        background: transparent;
      }
    }
  }

  &-item-label {
    font-weight: 500;
    color: var(--color-text);
  }

  &-item-description {
    font-size: var(--text-xs);
    color: var(--color-text-muted);
  }

  &-empty {
    padding: var(--space-8) var(--space-3);
    text-align: center;
    color: var(--color-text-muted);
    font-size: var(--text-sm);

    &[hidden] {
      display: none;
    }
  }

  &-loading {
    padding: var(--space-3);
    text-align: center;
    color: var(--color-text-muted);
    font-size: var(--text-sm);

    &[hidden] {
      display: none;
    }

    &::before {
      content: '';
      display: inline-block;
      width: 0.875rem;
      height: 0.875rem;
      margin-inline-end: var(--space-2);
      vertical-align: -2px;

      border: 2px solid var(--color-border);
      border-top-color: var(--color-accent);
      border-radius: var(--radius-full);

      animation: ren-combobox-spin var(--duration-slow, 800ms) linear infinite;

      @media (prefers-reduced-motion: reduce) {
        animation: none;
      }
    }
  }

  &-group {
    & + & {
      margin-top: var(--space-1);
      border-top: 1px solid var(--color-border);
      padding-top: var(--space-1);
    }

    &[hidden] {
      display: none;
    }
  }

  &-group-label {
    padding: var(--space-2) var(--space-3);
    padding-bottom: var(--space-1);

    font-size: var(--text-xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--color-text-muted);
  }
}

@keyframes ren-combobox-open {
  from {
    opacity: 0;
    translate: 0 -4px;
  }
  to {
    opacity: 1;
    translate: 0 0;
  }
}

@keyframes ren-combobox-spin {
  to {
    transform: rotate(360deg);
  }
}


id: file:components/composites/ren-combobox/ren-combobox.js
type: javascript
name: ren-combobox.js
path: components/composites/ren-combobox/ren-combobox.js
data: {}
body:
/**
 * RenDS — <ren-combobox> Web Component
 * =====================================
 * Searchable, accessible combobox built on light DOM.
 *
 * Progressive enhancement: the markup ships with classes that work
 * standalone (.ren-combobox, .ren-combobox-input, .ren-combobox-list,
 * .ren-combobox-item). The Web Component layers ARIA wiring, keyboard
 * navigation, filtering, and a hidden form input on top.
 *
 * No Shadow DOM — light DOM only, like the rest of RenDS.
 *
 * Markup:
 *   <ren-combobox name="country" placeholder="Search a country…" value="ar">
 *     <div class="ren-combobox-item" data-value="ar">Argentina</div>
 *     <div class="ren-combobox-item" data-value="br">Brazil</div>
 *     <div class="ren-combobox-item" data-value="cl">Chile</div>
 *   </ren-combobox>
 *
 * Attributes:
 *   name        — form field name (a hidden input with this name carries the value)
 *   value       — initial selected data-value
 *   placeholder — input placeholder (default: "Search...")
 *   disabled    — locks the input
 *   async       — suppresses local filtering; consumer provides items via ren-search
 *
 * Public methods:
 *   .value          — getter / setter for the selected value
 *   .open()         — show the list
 *   .close()        — hide the list
 *   .setLoading(b)  — toggle the loading row
 *   .setItems(arr)  — replace items, each { value, label, description? }
 *
 * Events (all bubble):
 *   ren-change  — { value, item }   selection changed
 *   ren-search  — { query }         user typed (use for async loading)
 *   ren-open / ren-close            list visibility
 */

import { autoId } from '../../../utils/id-generator.js';

const COMBOBOX_SIDES = new Set(['top', 'right', 'bottom', 'left']);

function normalizeComboboxSide(value) {
  const side = String(value || 'bottom').toLowerCase().split('-')[0];

  return COMBOBOX_SIDES.has(side) ? side : 'bottom';
}

export class RenCombobox extends HTMLElement {
  static get observedAttributes() {
    return ['value', 'placeholder', 'disabled', 'placement'];
  }

  #input;
  #list;
  #empty;
  #loading;
  #hidden;
  #liveRegion;
  #isOpen = false;
  #highlightedIndex = -1;
  #upgraded = false;
  #onDocClick;

  connectedCallback() {
    if (this.#upgraded) return;
    this.#upgraded = true;
    this.#enhance();
    this.#wire();

    // Apply initial value if present
    const initial = this.getAttribute('value');
    if (initial) {
      this.#selectByValue(initial, { silent: true });
    }
  }

  disconnectedCallback() {
    if (this.#onDocClick) {
      document.removeEventListener('click', this.#onDocClick);
    }
  }

  attributeChangedCallback(name, oldVal, newVal) {
    if (!this.#upgraded) return;
    if (name === 'placeholder' && this.#input) {
      this.#input.placeholder = newVal || 'Search...';
    } else if (name === 'disabled' && this.#input) {
      this.#input.disabled = this.hasAttribute('disabled');
    } else if (name === 'value' && newVal !== oldVal) {
      this.#selectByValue(newVal, { silent: true });
    } else if (name === 'placement' && newVal !== oldVal) {
      this.#syncPlacement();
    }
  }

  /* ─── Enhancement: take light-DOM children and wire structure ─── */

  #enhance() {
    this.classList.add('ren-combobox');

    // Items: anything the consumer wrote inside (items, groups, labels).
    // Move them into a list container.
    const fragment = document.createDocumentFragment();
    while (this.firstChild) {
      fragment.appendChild(this.firstChild);
    }

    // Input. type="search" + an unguessable autocomplete value defeats the
    // Chromium heuristic that autofills "country"-flavored fields.
    this.#input = document.createElement('input');
    this.#input.type = 'search';
    this.#input.className = 'ren-combobox-input';
    this.#input.setAttribute('autocomplete', 'ren-combobox-no-autofill');
    this.#input.setAttribute('autocorrect', 'off');
    this.#input.setAttribute('autocapitalize', 'off');
    this.#input.setAttribute('data-1p-ignore', '');
    this.#input.setAttribute('data-lpignore', 'true');
    this.#input.spellcheck = false;
    this.#input.placeholder = this.getAttribute('placeholder') || 'Search...';
    if (this.hasAttribute('disabled')) this.#input.disabled = true;
    autoId(this.#input, 'combobox-input');

    // List
    this.#list = document.createElement('div');
    this.#list.className = 'ren-combobox-list';
    this.#list.hidden = true;
    autoId(this.#list, 'combobox-list');

    // Move user content into the list
    this.#list.appendChild(fragment);

    // Ensure an empty-state row exists
    this.#empty = this.#list.querySelector('.ren-combobox-empty');
    if (!this.#empty) {
      this.#empty = document.createElement('div');
      this.#empty.className = 'ren-combobox-empty';
      this.#empty.textContent = 'No results';
      this.#empty.hidden = true;
      this.#list.appendChild(this.#empty);
    }

    // Loading row (hidden by default)
    this.#loading = this.#list.querySelector('.ren-combobox-loading');
    if (!this.#loading) {
      this.#loading = document.createElement('div');
      this.#loading.className = 'ren-combobox-loading';
      this.#loading.textContent = 'Loading…';
      this.#loading.hidden = true;
      this.#list.insertBefore(this.#loading, this.#list.firstChild);
    }

    // Hidden input for form submission
    this.#hidden = document.createElement('input');
    this.#hidden.type = 'hidden';
    if (this.hasAttribute('name')) this.#hidden.name = this.getAttribute('name');

    // Live region for screen-reader announcements
    this.#liveRegion = document.createElement('div');
    this.#liveRegion.className = 'ren-sr-only';
    this.#liveRegion.setAttribute('role', 'status');
    this.#liveRegion.setAttribute('aria-live', 'polite');

    // ARIA wiring
    this.#input.setAttribute('role', 'combobox');
    this.#input.setAttribute('aria-autocomplete', 'list');
    this.#input.setAttribute('aria-expanded', 'false');
    this.#input.setAttribute('aria-controls', this.#list.id);
    this.#list.setAttribute('role', 'listbox');

    this.#decorateItems();

    // Append in canonical order
    this.appendChild(this.#input);
    this.appendChild(this.#list);
    this.appendChild(this.#hidden);
    this.appendChild(this.#liveRegion);

    this.#syncPlacement();
  }

  #syncPlacement() {
    const side = normalizeComboboxSide(this.getAttribute('placement'));

    this.setAttribute('data-side', side);
    if (this.#list) {
      this.#list.setAttribute('data-side', side);
    }
  }

  #decorateItems() {
    const items = this.#getItems();
    items.forEach((item) => {
      if (!item.hasAttribute('role')) item.setAttribute('role', 'option');
      autoId(item, 'combobox-opt');
      if (!item.hasAttribute('aria-selected')) {
        item.setAttribute('aria-selected', 'false');
      }
      if (!item.hasAttribute('tabindex')) item.setAttribute('tabindex', '-1');
    });
  }

  #getItems() {
    return Array.from(this.#list.querySelectorAll('.ren-combobox-item'));
  }

  #getVisibleItems() {
    return this.#getItems().filter((i) => !i.hidden);
  }

  /* ─── Event wiring ─── */

  #wire() {
    // Open on click (not focus): programmatic .focus() after selection
    // shouldn't reopen the list.
    this.#input.addEventListener('mousedown', () => this.open());
    this.#input.addEventListener('input', (e) => this.#onInput(e));
    this.#input.addEventListener('keydown', (e) => this.#onKeydown(e));

    // Item selection (delegated)
    this.#list.addEventListener('click', (e) => {
      const item = e.target.closest('.ren-combobox-item');
      if (item && !item.hasAttribute('aria-disabled')) {
        this.#selectItem(item);
      }
    });

    this.#list.addEventListener('mouseover', (e) => {
      const item = e.target.closest('.ren-combobox-item');
      if (!item || item.hidden) return;
      const visible = this.#getVisibleItems();
      const idx = visible.indexOf(item);
      if (idx !== -1) this.#highlight(idx);
    });

    // Close on outside click
    this.#onDocClick = (e) => {
      if (!this.contains(e.target)) this.close();
    };
    document.addEventListener('click', this.#onDocClick);
  }

  #onInput(e) {
    const query = e.target.value;

    if (this.hasAttribute('async')) {
      // Async mode: emit ren-search and let the consumer replace items
      this.dispatchEvent(
        new CustomEvent('ren-search', {
          detail: { query },
          bubbles: true,
        })
      );
    } else {
      this.#filter(query);
    }

    if (!this.#isOpen) this.open();
  }

  #onKeydown(e) {
    const visible = this.#getVisibleItems();

    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault();
        if (!this.#isOpen) this.open();
        this.#highlight(Math.min(this.#highlightedIndex + 1, visible.length - 1));
        break;
      case 'ArrowUp':
        e.preventDefault();
        if (this.#isOpen) {
          this.#highlight(Math.max(this.#highlightedIndex - 1, 0));
        }
        break;
      case 'Home':
        if (this.#isOpen) {
          e.preventDefault();
          this.#highlight(0);
        }
        break;
      case 'End':
        if (this.#isOpen) {
          e.preventDefault();
          this.#highlight(visible.length - 1);
        }
        break;
      case 'Enter':
        if (this.#isOpen && visible[this.#highlightedIndex]) {
          e.preventDefault();
          this.#selectItem(visible[this.#highlightedIndex]);
        }
        break;
      case 'Escape':
        if (this.#isOpen) {
          e.preventDefault();
          this.close();
        } else if (this.#input.value) {
          this.#input.value = '';
          this.#filter('');
        }
        break;
    }
  }

  /* ─── Filtering ─── */

  #filter(query) {
    const needle = (query || '').toLowerCase().trim();
    const items = this.#getItems();

    items.forEach((item) => {
      const match = !needle || item.textContent.toLowerCase().includes(needle);
      item.hidden = !match;
    });

    // Hide groups whose items are all hidden
    this.querySelectorAll('.ren-combobox-group').forEach((group) => {
      const groupItems = group.querySelectorAll('.ren-combobox-item');
      const anyVisible = Array.from(groupItems).some((i) => !i.hidden);
      group.hidden = !anyVisible;
    });

    const anyVisible = items.some((i) => !i.hidden);
    this.#empty.hidden = anyVisible;
    this.#highlightedIndex = -1;

    // Clear ARIA active descendant
    this.#input.removeAttribute('aria-activedescendant');

    // Announce result count
    const count = items.filter((i) => !i.hidden).length;
    if (this.#liveRegion) {
      this.#liveRegion.textContent = count
        ? `${count} result${count === 1 ? '' : 's'} available`
        : 'No results';
    }
  }

  /* ─── Highlight management (aria-activedescendant pattern) ─── */

  #highlight(index) {
    const visible = this.#getVisibleItems();
    if (!visible.length) {
      this.#highlightedIndex = -1;
      this.#input.removeAttribute('aria-activedescendant');
      return;
    }

    this.#getItems().forEach((i) => i.removeAttribute('data-highlighted'));

    const clamped = Math.max(0, Math.min(index, visible.length - 1));
    const target = visible[clamped];
    target.setAttribute('data-highlighted', '');
    this.#input.setAttribute('aria-activedescendant', target.id);
    this.#highlightedIndex = clamped;

    // Best-effort scroll; not critical if unsupported.
    if (typeof target.scrollIntoView === 'function') {
      try {
        target.scrollIntoView({ block: 'nearest' });
      } catch {}
    }
  }

  /* ─── Selection ─── */

  #selectItem(item) {
    const value = item.getAttribute('data-value') || item.textContent.trim();
    const label = item.textContent.trim();

    this.#input.value = label;
    this.#hidden.value = value;
    this.setAttribute('value', value);

    this.#getItems().forEach((i) => {
      i.setAttribute('aria-selected', i === item ? 'true' : 'false');
    });

    this.close();
    this.#input.focus();

    this.dispatchEvent(
      new CustomEvent('ren-change', {
        detail: { value, item },
        bubbles: true,
      })
    );
  }

  #selectByValue(value, { silent = false } = {}) {
    const item = this.#getItems().find(
      (i) => i.getAttribute('data-value') === value
    );
    if (!item) return;

    if (silent) {
      this.#input.value = item.textContent.trim();
      this.#hidden.value = value;
      this.#getItems().forEach((i) => {
        i.setAttribute('aria-selected', i === item ? 'true' : 'false');
      });
    } else {
      this.#selectItem(item);
    }
  }

  /* ─── Public API ─── */

  open() {
    if (this.#isOpen) return;
    this.#isOpen = true;
    this.#list.hidden = false;
    this.#input.setAttribute('aria-expanded', 'true');
    this.dispatchEvent(new CustomEvent('ren-open', { bubbles: true }));
  }

  close() {
    if (!this.#isOpen) return;
    this.#isOpen = false;
    this.#list.hidden = true;
    this.#input.setAttribute('aria-expanded', 'false');
    this.#input.removeAttribute('aria-activedescendant');
    this.#getItems().forEach((i) => i.removeAttribute('data-highlighted'));
    this.#highlightedIndex = -1;
    this.dispatchEvent(new CustomEvent('ren-close', { bubbles: true }));
  }

  setLoading(isLoading) {
    if (!this.#loading) return;
    this.#loading.hidden = !isLoading;
    if (isLoading) this.#empty.hidden = true;
  }

  setItems(items) {
    // Clear existing items (preserve groups, empty, loading)
    this.#getItems().forEach((i) => i.remove());

    const frag = document.createDocumentFragment();
    items.forEach(({ value, label, description, disabled }) => {
      const item = document.createElement('div');
      item.className = 'ren-combobox-item';
      item.setAttribute('role', 'option');
      item.setAttribute('aria-selected', 'false');
      item.setAttribute('tabindex', '-1');
      autoId(item, 'combobox-opt');
      item.dataset.value = value;
      if (disabled) item.setAttribute('aria-disabled', 'true');

      if (description) {
        const main = document.createElement('div');
        main.className = 'ren-combobox-item-label';
        main.textContent = label;
        const desc = document.createElement('div');
        desc.className = 'ren-combobox-item-description';
        desc.textContent = description;
        item.append(main, desc);
      } else {
        item.textContent = label;
      }
      frag.appendChild(item);
    });

    this.#list.insertBefore(frag, this.#empty);
    this.#empty.hidden = items.length > 0;
    this.#highlightedIndex = -1;
  }

  get value() {
    return this.#hidden ? this.#hidden.value : '';
  }

  set value(val) {
    if (val == null || val === '') {
      if (this.#input) this.#input.value = '';
      if (this.#hidden) this.#hidden.value = '';
      this.#getItems().forEach((i) => i.setAttribute('aria-selected', 'false'));
      this.removeAttribute('value');
    } else {
      this.setAttribute('value', val);
    }
  }
}

if (!customElements.get('ren-combobox')) {
  customElements.define('ren-combobox', RenCombobox);
}


id: file:components/composites/ren-context-menu/ren-context-menu.css
type: css
name: ren-context-menu.css
path: components/composites/ren-context-menu/ren-context-menu.css
data: {}
body:
/* ============================================
   RenDS — Context Menu Component
   ============================================
   Right-click menu using Popover API.
   Reuses ren-menu styles for items.
   Minimal JS for right-click trigger + positioning.

   Usage:
     <div class="ren-context-menu-trigger" data-context="my-ctx-menu">
       Right-click this area
     </div>

     <div id="my-ctx-menu" class="ren-context-menu" popover>
       <button class="ren-menu-item">Cut</button>
       <button class="ren-menu-item">Copy</button>
       <button class="ren-menu-item">Paste</button>
       <hr class="ren-menu-separator">
       <button class="ren-menu-item ren-menu-item-danger">Delete</button>
     </div>
   ============================================ */

.ren-context-menu {
  /* Reset popover defaults */
  margin: 0;
  padding: 0;
  border: none;
  background: none;
  inset: auto;

  /* Menu appearance (matches ren-menu) */
  background: var(--color-surface);
  border: var(--stroke-1) solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  padding-block: var(--space-1);
  min-width: 10rem;
  max-height: calc(100vh - 32px);
  overflow-y: auto;
  overscroll-behavior: contain;

  /* Position set by JS */
  position: fixed;

  /* Animation — semantic tokens keep context-menu in sync with
     ren-menu / ren-popover. Collapses to 0ms under reduce. */
  opacity: 0;
  transform: scale(0.95);
  transform-origin: top left;
  transition:
    opacity var(--duration-enter) var(--ease-enter),
    transform var(--duration-enter) var(--ease-enter),
    overlay var(--duration-enter) var(--ease-enter) allow-discrete,
    display var(--duration-enter) var(--ease-enter) allow-discrete;
}

.ren-context-menu:popover-open,
.ren-context-menu.ren-open {
  opacity: 1;
  transform: scale(1);
}

@starting-style {
  .ren-context-menu:popover-open {
    opacity: 0;
    transform: scale(0.95);
  }
}

/* ─── Reuse menu item styles ─── */
/* Already defined in ren-menu.css:
   .ren-menu-item, .ren-menu-separator, etc.
   Import ren-menu.css alongside this file.
*/

/* ─── Reduced motion ─── */
@media (prefers-reduced-motion: reduce) {
  .ren-context-menu {
    transition: none;
  }
}


id: file:components/composites/ren-context-menu/ren-context-menu.js
type: javascript
name: ren-context-menu.js
path: components/composites/ren-context-menu/ren-context-menu.js
data: {}
body:
/* ============================================
   RenDS — Context Menu Controller
   ============================================
   Handles right-click trigger, positioning,
   and keyboard navigation.

   Usage:
     import { initContextMenu } from './ren-context-menu.js';
     initContextMenu('my-ctx-menu');

   Or auto-init all:
     import { initAllContextMenus } from './ren-context-menu.js';
     initAllContextMenus();
   ============================================ */

/**
 * @param {string|HTMLElement} menuOrId - The popover element or its ID
 */
export function initContextMenu(menuOrId) {
  const menu = typeof menuOrId === 'string'
    ? document.getElementById(menuOrId)
    : menuOrId;
  if (!menu) return;

  if (menu.dataset.renContextMenuInitialized === 'true') return;
  menu.dataset.renContextMenuInitialized = 'true';
  const controller = new AbortController();
  menu.__renContextController = controller;
  let returnTrigger = null;

  menu.setAttribute('popover', 'manual');
  menu.setAttribute('role', 'menu');
  menu.classList.add('ren-context-menu');

  // Find all triggers for this menu
  const triggers = [...document.querySelectorAll(`[data-context="${menu.id}"]`)];
  const triggerId = menu.getAttribute('trigger-id');
  const explicitTrigger = triggerId ? document.getElementById(triggerId) : null;
  if (explicitTrigger && !triggers.includes(explicitTrigger)) triggers.push(explicitTrigger);
  const itemSelector = [
    '.ren-menu-item:not(:disabled):not([aria-disabled="true"])',
    '[role="menuitem"]:not(:disabled):not([aria-disabled="true"])',
  ].join(', ');

  const isOpen = () => menu.matches(':popover-open') || menu.classList.contains('ren-open');

  const close = () => {
    if (!isOpen()) return;

    if ('hidePopover' in menu && menu.matches(':popover-open')) {
      try {
        menu.hidePopover();
      } catch {
        // Popover may have already been closed by the browser.
      }
    } else {
      menu.classList.remove('ren-open');
    }

    menu.setAttribute('data-state', 'closed');
    if (returnTrigger && document.contains(returnTrigger)) returnTrigger.focus();
  };

  const show = (x, y, target) => {
    if (isOpen()) {
      close();
    }

    // Position menu at cursor
    menu.style.left = `${x}px`;
    menu.style.top = `${y}px`;

    if ('showPopover' in menu) {
      try {
        menu.showPopover();
      } catch {
        // Popover may already be open during rapid repeated contextmenu events.
      }
    } else {
      menu.classList.add('ren-open');
    }

    menu.setAttribute('data-state', 'open');

    // Adjust if overflows viewport
    const rect = menu.getBoundingClientRect();
    if (rect.right > window.innerWidth) {
      menu.style.left = `${Math.max(8, x - rect.width)}px`;
    }
    if (rect.bottom > window.innerHeight) {
      menu.style.top = `${Math.max(8, y - rect.height)}px`;
    }

    // Focus first item
    const firstItem = menu.querySelector(itemSelector);
    firstItem?.focus();

    menu.dispatchEvent(new CustomEvent('ren-context-menu-open', {
      bubbles: true,
      composed: true,
      detail: { x, y, target },
    }));
  };

  triggers.forEach((trigger) => {
    trigger.addEventListener('contextmenu', (e) => {
      e.preventDefault();
      returnTrigger = trigger;
      show(e.clientX, e.clientY, e.target);
    }, { signal: controller.signal });

    trigger.addEventListener('keydown', (e) => {
      if (e.key !== 'ContextMenu' && !(e.key === 'F10' && e.shiftKey)) return;

      e.preventDefault();
      const rect = trigger.getBoundingClientRect();
      returnTrigger = trigger;
      show(rect.left + 8, rect.bottom + 8, e.target);
    }, { signal: controller.signal });
  });

  // Keyboard navigation inside menu
  menu.addEventListener('keydown', (e) => {
    const items = [...menu.querySelectorAll(itemSelector)];
    if (items.length === 0) return;

    const current = items.indexOf(document.activeElement);

    if (e.key === 'ArrowDown') {
      e.preventDefault();
      items[(current + 1) % items.length]?.focus();
    } else if (e.key === 'ArrowUp') {
      e.preventDefault();
      items[(current - 1 + items.length) % items.length]?.focus();
    } else if (e.key === 'Home') {
      e.preventDefault();
      items[0]?.focus();
    } else if (e.key === 'End') {
      e.preventDefault();
      items.at(-1)?.focus();
    } else if (e.key === 'Escape') {
      close();
    } else if (e.key === 'Enter' || e.key === ' ') {
      e.preventDefault();
      document.activeElement?.click();
      close();
    }
  }, { signal: controller.signal });

  // Close on item click
  menu.addEventListener('click', (e) => {
    if (e.target.closest('.ren-menu-item')) {
      close();
    }
  }, { signal: controller.signal });

  const isMenuOrTrigger = (target) =>
    target instanceof Node &&
    (menu.contains(target) || triggers.some((trigger) => trigger.contains(target)));

  document.addEventListener(
    'pointerdown',
    (e) => {
      if (isOpen() && !isMenuOrTrigger(e.target)) {
        close();
      }
    },
    { capture: true, signal: controller.signal }
  );

  document.addEventListener(
    'contextmenu',
    (e) => {
      if (isOpen() && !isMenuOrTrigger(e.target)) {
        close();
      }
    },
    { capture: true, signal: controller.signal }
  );
}

export function destroyContextMenu(menuOrId) {
  const menu = typeof menuOrId === 'string'
    ? document.getElementById(menuOrId)
    : menuOrId;
  if (!menu) return;
  menu.__renContextController?.abort();
  delete menu.__renContextController;
  delete menu.dataset.renContextMenuInitialized;
}

/**
 * Auto-init all context menus.
 */
export function initAllContextMenus() {
  const menuIds = new Set();
  document.querySelectorAll('[data-context]').forEach((el) => {
    menuIds.add(el.dataset.context);
  });
  menuIds.forEach(initContextMenu);
}

/**
 * Registry-friendly custom element facade over the same controller used by
 * initContextMenu(). There is one behavior owner and one reconnect-safe
 * AbortController regardless of which public entry point consumers use.
 */
export class RenContextMenu extends HTMLElement {
  connectedCallback() {
    if (!this.id) this.id = `ren-context-menu-${Math.random().toString(36).slice(2, 11)}`;
    initContextMenu(this);
  }

  disconnectedCallback() {
    destroyContextMenu(this);
  }
}

if (!customElements.get('ren-context-menu')) {
  customElements.define('ren-context-menu', RenContextMenu);
}


id: file:components/composites/ren-date-picker/ren-date-picker.css
type: css
name: ren-date-picker.css
path: components/composites/ren-date-picker/ren-date-picker.css
data: {}
body:
/* ═══ REN DATE PICKER WEB COMPONENT ═══
   A composable date picker that wraps the calendar component
   with an input field and popover dropdown.
   Uses Popover API for positioning with CSS anchor positioning.
   ══════════════════════════════════════════════════════════════════ */

/* ═══ BASE WRAPPER ═══ */
.ren-date-picker {
  display: inline-flex;
  flex-direction: column;
  width: 100%;
  max-width: 100%;
  position: relative;
}

/* ═══ TRIGGER INPUT ═══ */
.ren-date-picker-trigger {
  anchor-name: --ren-date-picker-anchor;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  width: 100%;
  padding: var(--space-2) var(--space-3);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  color: var(--color-text);
  font-size: var(--body-size);
  font-family: inherit;
  cursor: pointer;
  transition: var(--transition-tactile);
  min-height: var(--touch-min);

  &:hover:not([disabled]) {
    border-color: var(--color-border-strong);
  }

  &:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 2px;
    border-color: var(--color-accent);
  }

  &[disabled] {
    background: var(--color-fill);
    color: var(--color-text-muted);
    cursor: not-allowed;
    opacity: 0.6;
  }

  /* ═══ PLACEHOLDER TEXT ═══ */
  &::placeholder {
    color: var(--color-text-muted);
  }

  /* ═══ CALENDAR ICON (::after) ═══ */
  &::after {
    content: '📅';
    font-size: 1.25em;
    flex-shrink: 0;
    pointer-events: none;
  }
}

/* ═══ TRIGGER TEXT CONTENT ═══ */
.ren-date-picker-value {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ═══ EMPTY STATE ═══ */
.ren-date-picker-trigger.ren-date-picker-empty {
  color: var(--color-text-muted);

  .ren-date-picker-value {
    display: none;
  }

  &::before {
    content: attr(data-placeholder);
    color: var(--color-text-muted);
  }
}

/* ═══ DROPDOWN CONTAINER (POPOVER) ═══ */
.ren-date-picker-dropdown {
  position-anchor: --ren-date-picker-anchor;
  position: absolute;
  position-area: bottom span-all;
  position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
  inset: auto;
  margin: 0;
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  z-index: 1000;
  max-width: 400px;

  /* ═══ POPOVER API ═══ */
  &[popover] {
    padding: 0;
    margin: 0;
  }

  &:popover-open {
    background: var(--color-surface-raised);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg);
    animation: ren-date-picker-popup-in var(--duration-enter) var(--ease-enter) both;
  }
}

.ren-date-picker-dropdown[data-side="bottom"],
.ren-date-picker-dropdown:not([data-side]) {
  position-area: bottom span-all;
  margin-block-start: var(--space-2);
  margin-block-end: 0;
  margin-inline: 0;
}

.ren-date-picker-dropdown[data-side="top"] {
  position-area: top span-all;
  margin-block-start: 0;
  margin-block-end: var(--space-2);
  margin-inline: 0;
}

.ren-date-picker-dropdown[data-side="right"] {
  position-area: right span-all;
  margin-block: 0;
  margin-inline-start: var(--space-2);
  margin-inline-end: 0;
}

.ren-date-picker-dropdown[data-side="left"] {
  position-area: left span-all;
  margin-block: 0;
  margin-inline-start: 0;
  margin-inline-end: var(--space-2);
}

/* ═══ FALLBACK POSITIONING (no complete anchor support) ═══ */
@supports not ((anchor-name: --ren-anchor) and
  (position-anchor: --ren-anchor) and
  (position-area: bottom span-all)) {
  .ren-date-picker-dropdown {
    position: fixed;
    top: auto;
    left: auto;
  }
}

/* ═══ ANIMATION: POPUP IN ═══ */
@starting-style {
  .ren-date-picker-dropdown:popover-open {
    opacity: 0;
    translate: 0 -8px;
  }
}

@keyframes ren-date-picker-popup-in {
  from {
    opacity: 0;
    translate: 0 -8px;
  }

  to {
    opacity: 1;
    translate: 0 0;
  }
}

/* ═══ CALENDAR INSIDE DROPDOWN ═══ */
.ren-date-picker-dropdown .ren-calendar {
  border: none;
  box-shadow: none;
  background: var(--color-surface-raised);
  padding: var(--space-3);
}

/* ═══ PRESETS SECTION ═══ */
.ren-date-picker-presets {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  padding: var(--space-3);
  border-bottom: 1px solid var(--color-border);
  background: var(--color-surface);
}

.ren-date-picker-preset {
  padding: var(--space-1-5) var(--space-3);
  background: var(--color-fill);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  color: var(--color-text);
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  cursor: pointer;
  transition: var(--transition-tactile);

  &:hover {
    background: var(--color-fill-hover);
    border-color: var(--color-border-strong);
  }

  &:active {
    background: var(--color-fill-active);
  }

  &:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 2px;
  }

  &[disabled] {
    opacity: 0.5;
    cursor: not-allowed;
  }
}

/* ═══ RANGE MODE: SHOW START AND END ═══ */
.ren-date-picker.ren-date-picker-range .ren-date-picker-value::after {
  content: ' → ';
  margin: 0 var(--space-1);
}

/* ═══ DISABLED STATE ═══ */
.ren-date-picker[disabled] {
  opacity: 0.6;
  pointer-events: none;

  .ren-date-picker-trigger {
    cursor: not-allowed;
  }
}

/* ═══ REDUCED MOTION SUPPORT ═══ */
@media (prefers-reduced-motion: reduce) {
  .ren-date-picker-dropdown:popover-open {
    animation: none;
    opacity: 1;
    translate: 0 0;
  }

  .ren-date-picker-dropdown {
    transition: none;
  }
}

/* ═══ DARK MODE SUPPORT ═══ */
@media (prefers-color-scheme: dark) {
  .ren-date-picker-trigger,
  .ren-date-picker-dropdown {
    /* ═══ USES CSS VARIABLES - AUTO ADAPTS ═══ */
  }
}

/* ═══ MOBILE RESPONSIVENESS ═══ */
@media (max-width: 480px) {
  .ren-date-picker-dropdown {
    position: fixed !important;
    inset: auto 0 0 0 !important;
    max-width: none;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    max-height: 80vh;
    overflow-y: auto;

    &:popover-open {
      animation: ren-date-picker-mobile-in var(--duration-enter) var(--ease-enter) both;
    }
  }

  @starting-style {
    .ren-date-picker-dropdown:popover-open {
      translate: 0 100%;
      opacity: 0;
    }
  }

}

@keyframes ren-date-picker-mobile-in {
  from {
    translate: 0 100%;
    opacity: 0;
  }

  to {
    translate: 0 0;
    opacity: 1;
  }
}

/* ═══ ERROR STATE ═══ */
.ren-date-picker.ren-date-picker-error {
  .ren-date-picker-trigger {
    border-color: var(--color-danger);

    &:focus-visible {
      outline-color: var(--color-danger);
      border-color: var(--color-danger);
    }
  }
}

/* ═══ SUCCESS STATE ═══ */
.ren-date-picker.ren-date-picker-success {
  .ren-date-picker-trigger {
    border-color: var(--color-success);

    &:focus-visible {
      outline-color: var(--color-success);
      border-color: var(--color-success);
    }
  }
}

/* ═══ FORM INTEGRATION ═══ */
.ren-date-picker {
  /* ═══ ALLOW FORM STYLING ═══ */

  input[type='hidden'] {
    display: none;
  }
}


id: file:components/composites/ren-date-picker/ren-date-picker.js
type: javascript
name: ren-date-picker.js
path: components/composites/ren-date-picker/ren-date-picker.js
data: {}
body:
/* ═══ REN DATE PICKER WEB COMPONENT ═══
   A composable date picker that combines an input field with a calendar dropdown.

   Features:
   - Integrates RenCalendar component internally
   - Single and range date selection modes
   - Preset shortcuts (Today, Tomorrow, Next Week, etc.)
   - Popover API for dropdown positioning
   - Locale-aware date formatting
   - Focus management with blur/focus trapping
   - Form integration with hidden input

   Usage:
   <ren-date-picker placeholder="Select date" format="long" mode="single"></ren-date-picker>
  ══════════════════════════════════════════════════════════════════ */

const DATE_PICKER_SIDES = new Set(['top', 'right', 'bottom', 'left']);

function normalizeDatePickerSide(value) {
  const side = String(value || 'bottom').toLowerCase().split('-')[0];

  return DATE_PICKER_SIDES.has(side) ? side : 'bottom';
}

export class RenDatePicker extends HTMLElement {
  static observedAttributes = ['placement'];

  constructor() {
    super();

    /* ═══ STATE ═══ */
    this.calendar = null;
    this.dropdown = null;
    this.trigger = null;
    this.isOpen = false;
    this.selectedValue = null;
    this.selectedRange = null;

    /* ═══ CONFIGURATION ═══ */
    this.format = 'short'; // short, medium, long
    this.locale = 'en-US';
    this.mode = 'single'; // single, range
    this.placeholder = 'Select date';

    /* ═══ BIND METHODS ═══ */
    this.handleTriggerClick = this.handleTriggerClick.bind(this);
    this.handleTriggerKeyDown = this.handleTriggerKeyDown.bind(this);
    this.handleCalendarSelect = this.handleCalendarSelect.bind(this);
    this.handleDocumentClick = this.handleDocumentClick.bind(this);
  }

  connectedCallback() {
    /* ═══ READ ATTRIBUTES ═══ */
    this.mode = this.getAttribute('mode') || 'single';
    this.format = this.getAttribute('format') || 'short';
    this.locale = this.getAttribute('locale') || 'en-US';
    this.placeholder = this.getAttribute('placeholder') || 'Select date';

    const minAttr = this.getAttribute('min');
    const maxAttr = this.getAttribute('max');

    /* ═══ RENDER COMPONENT ═══ */
    this.render();

    /* ═══ SET UP TRIGGER (must come after render) ═══ */
    this.trigger = this.querySelector('.ren-date-picker-trigger');
    if (this.trigger) {
      this.trigger.addEventListener('click', this.handleTriggerClick);
      this.trigger.addEventListener('keydown', this.handleTriggerKeyDown);
    }

    /* ═══ SET UP DROPDOWN (must come after render, before calendar) ═══ */
    this.dropdown = this.querySelector('[popover]');
    if (!this.dropdown) {
      this.dropdown = document.createElement('div');
      this.dropdown.className = 'ren-date-picker-dropdown';
      this.dropdown.setAttribute('popover', 'manual');
      this.appendChild(this.dropdown);
    }

    /* ═══ SET UP CALENDAR (must come after dropdown) ═══ */
    this.calendar = this.querySelector('ren-calendar');
    if (!this.calendar) {
      this.calendar = document.createElement('ren-calendar');
      this.calendar.setAttribute('mode', this.mode);
      if (this.locale) this.calendar.setAttribute('locale', this.locale);
      if (minAttr) this.calendar.setAttribute('min', minAttr);
      if (maxAttr) this.calendar.setAttribute('max', maxAttr);
      this.dropdown.appendChild(this.calendar);
    }

    this.syncPlacement();

    /* ═══ LISTEN TO CALENDAR EVENTS ═══ */
    this.calendar.addEventListener('ren-date-select', this.handleCalendarSelect);

    /* ═══ LISTEN TO DOCUMENT CLICKS ═══ */
    document.addEventListener('click', this.handleDocumentClick);

    /* ═══ SET INITIAL VALUE ═══ */
    const valueAttr = this.getAttribute('value');
    if (valueAttr) {
      this.setValue(valueAttr);
    }

    /* ═══ ADD TO FORM IF PARENT IS FORM ═══ */
    this.addHiddenInput();
  }

  disconnectedCallback() {
    if (this.trigger) {
      this.trigger.removeEventListener('click', this.handleTriggerClick);
      this.trigger.removeEventListener('keydown', this.handleTriggerKeyDown);
    }

    if (this.calendar) {
      this.calendar.removeEventListener('ren-date-select', this.handleCalendarSelect);
    }

    document.removeEventListener('click', this.handleDocumentClick);
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'placement' && oldValue !== newValue) {
      this.syncPlacement();
    }
  }

  /* ═══ RENDER COMPONENT STRUCTURE ═══ */
  render() {
    this.classList.add('ren-date-picker');

    if (this.mode === 'range') {
      this.classList.add('ren-date-picker-range');
    }

    if (this.innerHTML.trim() === '') {
      /* ═══ CREATE TRIGGER ═══ */
      const trigger = document.createElement('button');
      trigger.className = 'ren-date-picker-trigger ren-date-picker-empty';
      trigger.setAttribute('type', 'button');
      trigger.setAttribute('aria-haspopup', 'dialog');
      trigger.setAttribute('aria-expanded', 'false');
      trigger.setAttribute('data-placeholder', this.placeholder);

      const valueSpan = document.createElement('span');
      valueSpan.className = 'ren-date-picker-value';
      trigger.appendChild(valueSpan);

      this.appendChild(trigger);

      /* ═══ CREATE DROPDOWN ═══ */
      const dropdown = document.createElement('div');
      dropdown.className = 'ren-date-picker-dropdown';
      dropdown.setAttribute('popover', 'manual');

      this.appendChild(dropdown);
    }
  }

  /* ═══ MIRROR PLACEMENT TO PUBLIC DATA ATTRIBUTES ═══ */
  syncPlacement() {
    const side = normalizeDatePickerSide(this.getAttribute('placement'));

    this.setAttribute('data-side', side);
    if (this.dropdown) {
      this.dropdown.setAttribute('data-side', side);
    }
  }

  /* ═══ ADD HIDDEN INPUT FOR FORM SUBMISSION ═══ */
  addHiddenInput() {
    let hidden = this.querySelector('input[type="hidden"]');
    if (!hidden) {
      hidden = document.createElement('input');
      hidden.type = 'hidden';
      hidden.name = this.getAttribute('name') || 'date-picker';
      this.appendChild(hidden);
    }
    this.hiddenInput = hidden;
  }

  /* ═══ HANDLE TRIGGER CLICK ═══ */
  handleTriggerClick(event) {
    event.preventDefault();
    event.stopPropagation();

    if (this.hasAttribute('disabled')) return;

    if (this.isOpen) {
      this.close();
    } else {
      this.open();
    }
  }

  /* ═══ HANDLE TRIGGER KEYBOARD ═══ */
  handleTriggerKeyDown(event) {
    if (this.hasAttribute('disabled')) return;

    if (event.key === 'Enter' || event.key === ' ') {
      event.preventDefault();
      this.open();
    }

    if (event.key === 'Escape') {
      this.close();
    }
  }

  /* ═══ HANDLE CALENDAR DATE SELECT ═══ */
  handleCalendarSelect(event) {
    const detail = event.detail;

    if (this.mode === 'single') {
      this.selectedValue = detail.date;
      this.updateTrigger(this.formatDate(detail.date));
      this.dispatchChangeEvent();
      this.close();
    } else if (this.mode === 'range') {
      this.selectedRange = detail.range;
      if (detail.range && detail.range.start && detail.range.end) {
        const formattedRange = `${this.formatDate(detail.range.start)} → ${this.formatDate(detail.range.end)}`;
        this.updateTrigger(formattedRange);
        this.dispatchChangeEvent();
        this.close();
      } else {
        const start = detail.range?.start ? this.formatDate(detail.range.start) : '';
        this.updateTrigger(start);
      }
    }
  }

  /* ═══ HANDLE DOCUMENT CLICK (CLOSE DROPDOWN ON OUTSIDE CLICK) ═══ */
  handleDocumentClick(event) {
    if (!this.contains(event.target) && this.isOpen) {
      this.close();
    }
  }

  /* ═══ OPEN DROPDOWN ═══ */
  open() {
    if (this.isOpen || this.hasAttribute('disabled')) return;

    if (!this.dropdown) return;

    /* ═══ SET UP POPOVER AND POSITIONING ═══ */
    this.syncPlacement();

    try {
      this.dropdown.showPopover();
    } catch {
      /* ═══ FALLBACK FOR UNSUPPORTED BROWSERS ═══ */
      this.dropdown.style.display = 'block';
    }

    this.isOpen = true;

    if (this.trigger) {
      this.trigger.setAttribute('aria-expanded', 'true');
    }

    /* ═══ FOCUS CALENDAR ═══ */
    setTimeout(() => {
      if (this.calendar) {
        const firstDay = this.calendar.querySelector('.ren-calendar-day:not([disabled])');
        if (firstDay) {
          firstDay.focus();
        }
      }
    }, 100);

    this.dispatchEvent(
      new CustomEvent('ren-date-picker-open', {
        bubbles: true,
        composed: true,
      })
    );
  }

  /* ═══ CLOSE DROPDOWN ═══ */
  close() {
    if (!this.isOpen) return;

    if (!this.dropdown) return;

    try {
      this.dropdown.hidePopover();
    } catch {
      /* ═══ FALLBACK FOR UNSUPPORTED BROWSERS ═══ */
      this.dropdown.style.display = 'none';
    }

    this.isOpen = false;

    if (this.trigger) {
      this.trigger.setAttribute('aria-expanded', 'false');
      this.trigger.focus();
    }

    this.dispatchEvent(
      new CustomEvent('ren-date-picker-close', {
        bubbles: true,
        composed: true,
      })
    );
  }

  /* ═══ UPDATE TRIGGER TEXT ═══ */
  updateTrigger(formattedValue) {
    if (!this.trigger) return;

    const valueSpan = this.trigger.querySelector('.ren-date-picker-value');
    if (valueSpan) {
      valueSpan.textContent = formattedValue;
    }

    this.trigger.classList.remove('ren-date-picker-empty');

    /* ═══ UPDATE HIDDEN INPUT ═══ */
    if (this.hiddenInput) {
      this.hiddenInput.value = formattedValue;
    }
  }

  /* ═══ FORMAT DATE FOR DISPLAY ═══ */
  formatDate(date) {
    if (!date) return '';

    const formatter = new Intl.DateTimeFormat(this.locale, {
      year: 'numeric',
      month: this.format === 'short' ? '2-digit' : this.format === 'long' ? 'long' : 'short',
      day: '2-digit',
    });

    return formatter.format(date);
  }

  /* ═══ DISPATCH CHANGE EVENT ═══ */
  dispatchChangeEvent() {
    let value = null;
    let formattedValue = '';

    if (this.mode === 'single') {
      value = this.selectedValue ? this.dateToString(this.selectedValue) : null;
      formattedValue = this.selectedValue ? this.formatDate(this.selectedValue) : '';
    } else if (this.mode === 'range') {
      if (this.selectedRange && this.selectedRange.start && this.selectedRange.end) {
        value = {
          start: this.dateToString(this.selectedRange.start),
          end: this.dateToString(this.selectedRange.end),
        };
        formattedValue = `${this.formatDate(this.selectedRange.start)} → ${this.formatDate(this.selectedRange.end)}`;
      }
    }

    this.dispatchEvent(
      new CustomEvent('ren-change', {
        detail: {
          value,
          formattedValue,
        },
        bubbles: true,
        composed: true,
      })
    );
  }

  /* ═══ CONVERT DATE TO ISO STRING ═══ */
  dateToString(date) {
    const year = date.getFullYear();
    const month = String(date.getMonth() + 1).padStart(2, '0');
    const day = String(date.getDate()).padStart(2, '0');
    return `${year}-${month}-${day}`;
  }

  parseDate(value) {
    if (value instanceof Date) return new Date(value.getTime());
    const match = String(value).match(/^(\d{4})-(\d{2})-(\d{2})$/);
    return match ? new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3])) : new Date(value);
  }

  /* ═══ PUBLIC METHODS ═══ */

  /* ═══ GET VALUE ═══ */
  getValue() {
    if (this.mode === 'single') {
      return this.selectedValue ? this.dateToString(this.selectedValue) : null;
    } else if (this.mode === 'range') {
      return this.selectedRange
        ? {
            start: this.dateToString(this.selectedRange.start),
            end: this.dateToString(this.selectedRange.end),
          }
        : null;
    }
  }

  /* ═══ SET VALUE ═══ */
  setValue(value) {
    if (this.mode === 'single' && typeof value === 'string') {
      this.selectedValue = this.parseDate(value);
      this.calendar.setValue(value);
      this.updateTrigger(this.formatDate(this.selectedValue));
    } else if (this.mode === 'range' && value && typeof value === 'object') {
      this.selectedRange = {
        start: this.parseDate(value.start),
        end: this.parseDate(value.end),
      };
      this.calendar.setRange(value.start, value.end);
      const formatted = `${this.formatDate(this.selectedRange.start)} → ${this.formatDate(this.selectedRange.end)}`;
      this.updateTrigger(formatted);
    }
  }

  /* ═══ TOGGLE DROPDOWN ═══ */
  toggle() {
    if (this.isOpen) {
      this.close();
    } else {
      this.open();
    }
  }

  /* ═══ GETTERS AND SETTERS ═══ */
  get disabled() {
    return this.hasAttribute('disabled');
  }

  set disabled(val) {
    if (val) {
      this.setAttribute('disabled', '');
      if (this.trigger) this.trigger.disabled = true;
    } else {
      this.removeAttribute('disabled');
      if (this.trigger) this.trigger.disabled = false;
    }
  }

  get isOpened() {
    return this.isOpen;
  }

  set isOpened(val) {
    if (val) {
      this.open();
    } else {
      this.close();
    }
  }
}

/* ═══ REGISTER COMPONENT ═══ */
if (!customElements.get('ren-date-picker')) {
  customElements.define('ren-date-picker', RenDatePicker);
}


id: file:components/composites/ren-date-range-picker/ren-date-range-picker.css
type: css
name: ren-date-range-picker.css
path: components/composites/ren-date-range-picker/ren-date-range-picker.css
data: {}
body:
/* ═══ REN DATE RANGE PICKER WEB COMPONENT ═══
   A composable date range picker with dual calendars,
   preset shortcuts, and an input trigger with popover dropdown.
   Built on top of RenCalendar for consistent behavior.

   Usage:
     <ren-date-range-picker
       placeholder="Select date range"
       format="short"
       locale="en-US"
       presets="last7,last30,thisMonth,lastMonth"
     ></ren-date-range-picker>
   ══════════════════════════════════════════════════════════════════ */

/* ═══ BASE WRAPPER ═══ */
.ren-date-range-picker {
  display: inline-flex;
  flex-direction: column;
  width: 100%;
  max-width: 100%;
  position: relative;
}

/* ═══ TRIGGER BUTTON ═══ */
.ren-date-range-trigger {
  anchor-name: --ren-date-range-anchor;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  width: 100%;
  padding: var(--space-2) var(--space-3);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  color: var(--color-text);
  font-size: var(--body-size);
  font-family: inherit;
  cursor: pointer;
  transition: var(--transition-tactile);
  min-height: var(--touch-min);

  &:hover:not([disabled]) {
    border-color: var(--color-border-strong);
  }

  &:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 2px;
    border-color: var(--color-accent);
  }

  &[disabled] {
    background: var(--color-fill);
    color: var(--color-text-muted);
    cursor: not-allowed;
    opacity: 0.6;
  }

  /* ═══ CALENDAR ICON ═══ */
  &::after {
    content: '📅';
    font-size: 1.25em;
    flex-shrink: 0;
    pointer-events: none;
  }
}

/* ═══ TRIGGER VALUE DISPLAY ═══ */
.ren-date-range-value {
  flex: 1;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  overflow: hidden;
  white-space: nowrap;
}

.ren-date-range-start,
.ren-date-range-end {
  overflow: hidden;
  text-overflow: ellipsis;
}

.ren-date-range-separator {
  color: var(--color-text-muted);
  flex-shrink: 0;
}

/* ═══ EMPTY STATE ═══ */
.ren-date-range-trigger.ren-date-range-empty {
  color: var(--color-text-muted);

  .ren-date-range-value {
    display: none;
  }

  &::before {
    content: attr(data-placeholder);
    color: var(--color-text-muted);
    flex: 1;
  }
}

/* ═══ DROPDOWN CONTAINER (POPOVER) ═══ */
.ren-date-range-dropdown {
  position-anchor: --ren-date-range-anchor;
  position: absolute;
  position-area: bottom span-all;
  position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
  inset: auto;
  margin: 0;
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  z-index: 1000;

  /* ═══ POPOVER API ═══ */
  &[popover] {
    padding: 0;
    margin: 0;
    border: none;
    background: transparent;
    box-shadow: none;
  }

  &:popover-open {
    animation: ren-date-range-popup-in var(--duration-enter) var(--ease-enter) both;
  }
}

.ren-date-range-dropdown[data-side="bottom"],
.ren-date-range-dropdown:not([data-side]) {
  position-area: bottom span-all;
  margin-block-start: var(--space-2);
  margin-block-end: 0;
  margin-inline: 0;
}

.ren-date-range-dropdown[data-side="top"] {
  position-area: top span-all;
  margin-block-start: 0;
  margin-block-end: var(--space-2);
  margin-inline: 0;
}

.ren-date-range-dropdown[data-side="right"] {
  position-area: right span-all;
  margin-block: 0;
  margin-inline-start: var(--space-2);
  margin-inline-end: 0;
}

.ren-date-range-dropdown[data-side="left"] {
  position-area: left span-all;
  margin-block: 0;
  margin-inline-start: 0;
  margin-inline-end: var(--space-2);
}

/* ═══ FALLBACK POSITIONING (no complete anchor support) ═══ */
@supports not ((anchor-name: --ren-anchor) and
  (position-anchor: --ren-anchor) and
  (position-area: bottom span-all)) {
  .ren-date-range-dropdown {
    position: fixed;
    top: auto;
    left: auto;
  }
}

/* ═══ ANIMATION: POPUP IN ═══ */
@starting-style {
  .ren-date-range-dropdown:popover-open {
    opacity: 0;
    translate: 0 -8px;
  }
}

@keyframes ren-date-range-popup-in {
  from {
    opacity: 0;
    translate: 0 -8px;
  }

  to {
    opacity: 1;
    translate: 0 0;
  }
}

/* ═══ DROPDOWN INNER WRAPPER ═══ */
.ren-date-range-inner {
  container-type: inline-size;
  container-name: ren-date-range;
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
}

/* ═══ PRESETS SIDEBAR ═══ */
.ren-date-range-presets {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-3);
  border-bottom: 1px solid var(--color-border);
  background: var(--color-surface);
}

.ren-date-range-preset {
  display: block;
  width: 100%;
  padding: var(--space-1-5) var(--space-3);
  background: transparent;
  border: none;
  border-radius: var(--radius-md);
  color: var(--color-text);
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  text-align: start;
  cursor: pointer;
  transition: var(--transition-tactile);

  &:hover {
    background: var(--color-fill);
  }

  &:active {
    background: var(--color-fill-active);
  }

  &:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 2px;
  }

  &[aria-selected="true"] {
    background: var(--color-accent-subtle);
    color: var(--color-accent);
    font-weight: var(--weight-semibold);
  }
}

/* ═══ DUAL CALENDAR LAYOUT ═══ */
.ren-date-range-calendars {
  display: flex;
  gap: 0;
  padding: var(--space-3);
}

.ren-date-range-calendars > .ren-calendar {
  border: none;
  box-shadow: none;
  background: transparent;
  flex: 1;
}

/* ═══ DIVIDER BETWEEN CALENDARS ═══ */
.ren-date-range-divider {
  width: 1px;
  align-self: stretch;
  background: var(--color-border);
  margin-block: var(--space-2);
  flex-shrink: 0;
}

/* ═══ FOOTER WITH ACTIONS ═══ */
.ren-date-range-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-3);
  border-top: 1px solid var(--color-border);
  background: var(--color-surface);
}

.ren-date-range-summary {
  font-size: var(--label-size);
  color: var(--color-text-secondary);
}

.ren-date-range-summary strong {
  color: var(--color-text);
  font-weight: var(--weight-semibold);
}

.ren-date-range-actions {
  display: flex;
  gap: var(--space-2);
}

.ren-date-range-cancel,
.ren-date-range-apply {
  padding: var(--space-1-5) var(--space-3);
  border-radius: var(--radius-md);
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  cursor: pointer;
  transition: var(--transition-tactile);

  &:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 2px;
  }
}

.ren-date-range-cancel {
  background: transparent;
  border: 1px solid var(--color-border);
  color: var(--color-text);

  &:hover {
    background: var(--color-fill);
    border-color: var(--color-border-strong);
  }
}

.ren-date-range-apply {
  background: var(--color-accent);
  border: 1px solid var(--color-accent);
  color: var(--color-on-accent);

  &:hover {
    background: var(--color-accent-hover);
  }

  &[disabled] {
    opacity: 0.5;
    cursor: not-allowed;
  }
}

/* ═══ DISABLED STATE ═══ */
.ren-date-range-picker[disabled] {
  opacity: 0.6;
  pointer-events: none;
}

/* ═══ ERROR STATE ═══ */
.ren-date-range-picker.ren-date-range-error {
  .ren-date-range-trigger {
    border-color: var(--color-danger);

    &:focus-visible {
      outline-color: var(--color-danger);
    }
  }
}

/* ═══ SUCCESS STATE ═══ */
.ren-date-range-picker.ren-date-range-success {
  .ren-date-range-trigger {
    border-color: var(--color-success);

    &:focus-visible {
      outline-color: var(--color-success);
    }
  }
}

/* ═══ REDUCED MOTION ═══ */
@media (prefers-reduced-motion: reduce) {
  .ren-date-range-dropdown:popover-open {
    animation: none;
    opacity: 1;
    translate: 0 0;
  }

  .ren-date-range-dropdown {
    transition: none;
  }
}

/* ═══ COMPACT LAYOUT (Container Query) ═══
   Stacks calendars vertically and makes presets
   horizontal when the dropdown is narrow —
   works in sidebars, modals, or small viewports. */
@container ren-date-range (max-width: 500px) {
  .ren-date-range-calendars {
    flex-direction: column;
    gap: var(--space-2);
  }

  .ren-date-range-divider {
    width: 100%;
    height: 1px;
    margin-inline: var(--space-2);
    margin-block: 0;
  }

  .ren-date-range-presets {
    flex-direction: row;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
  }

  .ren-date-range-preset {
    white-space: nowrap;
    flex-shrink: 0;
  }
}

/* ═══ MOBILE BOTTOM SHEET (Media Query) ═══
   Fixed positioning requires viewport awareness,
   so this stays as a media query. */
@media (max-width: 680px) {
  .ren-date-range-dropdown {
    position: fixed !important;
    inset: auto 0 0 0 !important;
    max-width: none;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    max-height: 85vh;
    overflow-y: auto;

    &:popover-open {
      animation: ren-date-range-mobile-in var(--duration-enter) var(--ease-enter) both;
    }
  }

  @starting-style {
    .ren-date-range-dropdown:popover-open {
      translate: 0 100%;
      opacity: 0;
    }
  }

}

@keyframes ren-date-range-mobile-in {
  from {
    translate: 0 100%;
    opacity: 0;
  }

  to {
    translate: 0 0;
    opacity: 1;
  }
}

/* ═══ FORM INTEGRATION ═══ */
.ren-date-range-picker {
  input[type='hidden'] {
    display: none;
  }
}


id: file:components/composites/ren-date-range-picker/ren-date-range-picker.js
type: javascript
name: ren-date-range-picker.js
path: components/composites/ren-date-range-picker/ren-date-range-picker.js
data: {}
body:
/* ═══ REN DATE RANGE PICKER WEB COMPONENT ═══
   A composable date range picker with dual side-by-side calendars,
   preset shortcuts, and confirmation actions.

   Features:
   - Dual calendar view (current + next month)
   - Configurable preset shortcuts (Last 7 days, Last 30 days, etc.)
   - Apply / Cancel workflow for intentional selection
   - Locale-aware date formatting via Intl.DateTimeFormat
   - Popover API dropdown with CSS anchor positioning
   - Full keyboard navigation (delegates to RenCalendar)
   - Focus management and outside-click dismissal
   - Form integration with hidden inputs (start + end)
   - Min/max date constraints

   Dependencies:
   - RenCalendar (ren-calendar.js) must be registered

   Usage:
   <ren-date-range-picker
     placeholder="Select date range"
     format="short"
     locale="en-US"
     presets="last7,last30,thisMonth,lastMonth,thisYear"
     min="2024-01-01"
     max="2027-12-31"
   ></ren-date-range-picker>

   Events:
   - ren-change: { value: { start, end }, formattedValue, preset? }
   - ren-date-range-open
   - ren-date-range-close
   ══════════════════════════════════════════════════════════════════ */

/* ═══ PRESET DEFINITIONS ═══ */
const PRESETS = {
  today: {
    label: 'Today',
    getRange: () => {
      const now = new Date();
      const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
      return { start: today, end: today };
    },
  },
  yesterday: {
    label: 'Yesterday',
    getRange: () => {
      const now = new Date();
      const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
      return { start: yesterday, end: yesterday };
    },
  },
  last7: {
    label: 'Last 7 days',
    getRange: () => {
      const now = new Date();
      const end = new Date(now.getFullYear(), now.getMonth(), now.getDate());
      const start = new Date(end);
      start.setDate(start.getDate() - 6);
      return { start, end };
    },
  },
  last14: {
    label: 'Last 14 days',
    getRange: () => {
      const now = new Date();
      const end = new Date(now.getFullYear(), now.getMonth(), now.getDate());
      const start = new Date(end);
      start.setDate(start.getDate() - 13);
      return { start, end };
    },
  },
  last30: {
    label: 'Last 30 days',
    getRange: () => {
      const now = new Date();
      const end = new Date(now.getFullYear(), now.getMonth(), now.getDate());
      const start = new Date(end);
      start.setDate(start.getDate() - 29);
      return { start, end };
    },
  },
  last90: {
    label: 'Last 90 days',
    getRange: () => {
      const now = new Date();
      const end = new Date(now.getFullYear(), now.getMonth(), now.getDate());
      const start = new Date(end);
      start.setDate(start.getDate() - 89);
      return { start, end };
    },
  },
  thisWeek: {
    label: 'This week',
    getRange: () => {
      const now = new Date();
      const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
      const dayOfWeek = today.getDay();
      const start = new Date(today);
      start.setDate(start.getDate() - dayOfWeek);
      const end = new Date(start);
      end.setDate(end.getDate() + 6);
      return { start, end };
    },
  },
  lastWeek: {
    label: 'Last week',
    getRange: () => {
      const now = new Date();
      const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
      const dayOfWeek = today.getDay();
      const end = new Date(today);
      end.setDate(end.getDate() - dayOfWeek - 1);
      const start = new Date(end);
      start.setDate(start.getDate() - 6);
      return { start, end };
    },
  },
  thisMonth: {
    label: 'This month',
    getRange: () => {
      const now = new Date();
      const start = new Date(now.getFullYear(), now.getMonth(), 1);
      const end = new Date(now.getFullYear(), now.getMonth() + 1, 0);
      return { start, end };
    },
  },
  lastMonth: {
    label: 'Last month',
    getRange: () => {
      const now = new Date();
      const start = new Date(now.getFullYear(), now.getMonth() - 1, 1);
      const end = new Date(now.getFullYear(), now.getMonth(), 0);
      return { start, end };
    },
  },
  thisQuarter: {
    label: 'This quarter',
    getRange: () => {
      const now = new Date();
      const quarterStart = Math.floor(now.getMonth() / 3) * 3;
      const start = new Date(now.getFullYear(), quarterStart, 1);
      const end = new Date(now.getFullYear(), quarterStart + 3, 0);
      return { start, end };
    },
  },
  thisYear: {
    label: 'This year',
    getRange: () => {
      const now = new Date();
      const start = new Date(now.getFullYear(), 0, 1);
      const end = new Date(now.getFullYear(), 11, 31);
      return { start, end };
    },
  },
  lastYear: {
    label: 'Last year',
    getRange: () => {
      const now = new Date();
      const start = new Date(now.getFullYear() - 1, 0, 1);
      const end = new Date(now.getFullYear() - 1, 11, 31);
      return { start, end };
    },
  },
};

const DATE_RANGE_SIDES = new Set(['top', 'right', 'bottom', 'left']);

function normalizeDateRangeSide(value) {
  const side = String(value || 'bottom').toLowerCase().split('-')[0];

  return DATE_RANGE_SIDES.has(side) ? side : 'bottom';
}

export class RenDateRangePicker extends HTMLElement {
  static observedAttributes = ['placement'];

  constructor() {
    super();

    /* ═══ STATE ═══ */
    this.calendarLeft = null;
    this.calendarRight = null;
    this.dropdown = null;
    this.trigger = null;
    this.isOpen = false;

    /* ═══ CONFIRMED (applied) range ═══ */
    this.confirmedStart = null;
    this.confirmedEnd = null;

    /* ═══ DRAFT (in-progress) range ═══ */
    this.draftStart = null;
    this.draftEnd = null;
    this.activePreset = null;

    /* ═══ CONFIGURATION ═══ */
    this.format = 'short';
    this.locale = 'en-US';
    this.placeholder = 'Select date range';
    this.presetKeys = ['last7', 'last30', 'thisMonth', 'lastMonth'];
    this.minDate = null;
    this.maxDate = null;

    /* ═══ CALENDAR MONTH OFFSETS ═══ */
    this.leftMonth = new Date();
    this.rightMonth = new Date(
      this.leftMonth.getFullYear(),
      this.leftMonth.getMonth() + 1
    );

    /* ═══ BIND METHODS ═══ */
    this.handleTriggerClick = this.handleTriggerClick.bind(this);
    this.handleTriggerKeyDown = this.handleTriggerKeyDown.bind(this);
    this.handleDocumentClick = this.handleDocumentClick.bind(this);
    this.handleCalendarSelect = this.handleCalendarSelect.bind(this);
    this.handleApply = this.handleApply.bind(this);
    this.handleCancel = this.handleCancel.bind(this);
  }

  connectedCallback() {
    /* ═══ READ ATTRIBUTES ═══ */
    this.format = this.getAttribute('format') || 'short';
    this.locale = this.getAttribute('locale') || 'en-US';
    this.placeholder = this.getAttribute('placeholder') || 'Select date range';

    const presetsAttr = this.getAttribute('presets');
    if (presetsAttr) {
      this.presetKeys = presetsAttr.split(',').map((k) => k.trim());
    }

    const minAttr = this.getAttribute('min');
    if (minAttr) this.minDate = minAttr;

    const maxAttr = this.getAttribute('max');
    if (maxAttr) this.maxDate = maxAttr;

    /* ═══ PARSE INITIAL VALUE ═══ */
    const startAttr = this.getAttribute('start');
    const endAttr = this.getAttribute('end');
    if (startAttr && endAttr) {
      this.confirmedStart = this.parseDate(startAttr);
      this.confirmedEnd = this.parseDate(endAttr);
      this.draftStart = this.parseDate(startAttr);
      this.draftEnd = this.parseDate(endAttr);
      this.leftMonth = new Date(this.confirmedStart.getFullYear(), this.confirmedStart.getMonth());
      this.rightMonth = new Date(this.leftMonth.getFullYear(), this.leftMonth.getMonth() + 1);
    }

    /* ═══ RENDER ═══ */
    this.render();
    this.setupEventListeners();
    this.addHiddenInputs();
    this.updateTriggerDisplay();
  }

  disconnectedCallback() {
    if (this.trigger) {
      this.trigger.removeEventListener('click', this.handleTriggerClick);
      this.trigger.removeEventListener('keydown', this.handleTriggerKeyDown);
    }

    document.removeEventListener('click', this.handleDocumentClick);
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'placement' && oldValue !== newValue) {
      this.syncPlacement();
    }
  }

  /* ═══ RENDER FULL COMPONENT ═══ */
  render() {
    this.innerHTML = '';
    this.classList.add('ren-date-range-picker');

    /* ═══ TRIGGER BUTTON ═══ */
    const trigger = document.createElement('button');
    trigger.className = 'ren-date-range-trigger ren-date-range-empty';
    trigger.setAttribute('type', 'button');
    trigger.setAttribute('aria-haspopup', 'dialog');
    trigger.setAttribute('aria-expanded', 'false');
    trigger.setAttribute('data-placeholder', this.placeholder);

    const valueEl = document.createElement('span');
    valueEl.className = 'ren-date-range-value';

    const startEl = document.createElement('span');
    startEl.className = 'ren-date-range-start';

    const sepEl = document.createElement('span');
    sepEl.className = 'ren-date-range-separator';
    sepEl.textContent = '→';

    const endEl = document.createElement('span');
    endEl.className = 'ren-date-range-end';

    valueEl.appendChild(startEl);
    valueEl.appendChild(sepEl);
    valueEl.appendChild(endEl);
    trigger.appendChild(valueEl);
    this.appendChild(trigger);
    this.trigger = trigger;

    /* ═══ DROPDOWN (POPOVER) ═══ */
    const dropdown = document.createElement('div');
    dropdown.className = 'ren-date-range-dropdown';
    dropdown.setAttribute('popover', 'manual');
    dropdown.setAttribute('role', 'dialog');
    dropdown.setAttribute('aria-label', 'Date range selection');

    const inner = document.createElement('div');
    inner.className = 'ren-date-range-inner';

    /* ═══ PRESETS ═══ */
    if (this.presetKeys.length > 0) {
      const presetsEl = document.createElement('div');
      presetsEl.className = 'ren-date-range-presets';
      presetsEl.setAttribute('role', 'listbox');
      presetsEl.setAttribute('aria-label', 'Date range presets');

      this.presetKeys.forEach((key) => {
        const preset = PRESETS[key];
        if (!preset) return;

        const btn = document.createElement('button');
        btn.className = 'ren-date-range-preset';
        btn.setAttribute('type', 'button');
        btn.setAttribute('role', 'option');
        btn.setAttribute('data-preset', key);
        btn.textContent = preset.label;

        btn.addEventListener('click', () => this.applyPreset(key));
        presetsEl.appendChild(btn);
      });

      inner.appendChild(presetsEl);
    }

    /* ═══ DUAL CALENDARS ═══ */
    const calendarsEl = document.createElement('div');
    calendarsEl.className = 'ren-date-range-calendars';

    const calLeft = document.createElement('ren-calendar');
    calLeft.setAttribute('mode', 'range');
    calLeft.setAttribute('locale', this.locale);
    if (this.minDate) calLeft.setAttribute('min', this.minDate);
    if (this.maxDate) calLeft.setAttribute('max', this.maxDate);

    const divider = document.createElement('div');
    divider.className = 'ren-date-range-divider';
    divider.setAttribute('aria-hidden', 'true');

    const calRight = document.createElement('ren-calendar');
    calRight.setAttribute('mode', 'range');
    calRight.setAttribute('locale', this.locale);
    if (this.minDate) calRight.setAttribute('min', this.minDate);
    if (this.maxDate) calRight.setAttribute('max', this.maxDate);

    calendarsEl.appendChild(calLeft);
    calendarsEl.appendChild(divider);
    calendarsEl.appendChild(calRight);
    inner.appendChild(calendarsEl);

    /* ═══ FOOTER WITH SUMMARY + ACTIONS ═══ */
    const footer = document.createElement('div');
    footer.className = 'ren-date-range-footer';

    const summary = document.createElement('div');
    summary.className = 'ren-date-range-summary';
    summary.setAttribute('aria-live', 'polite');

    const actions = document.createElement('div');
    actions.className = 'ren-date-range-actions';

    const cancelBtn = document.createElement('button');
    cancelBtn.className = 'ren-date-range-cancel';
    cancelBtn.setAttribute('type', 'button');
    cancelBtn.textContent = 'Cancel';

    const applyBtn = document.createElement('button');
    applyBtn.className = 'ren-date-range-apply';
    applyBtn.setAttribute('type', 'button');
    applyBtn.textContent = 'Apply';
    applyBtn.disabled = true;

    actions.appendChild(cancelBtn);
    actions.appendChild(applyBtn);
    footer.appendChild(summary);
    footer.appendChild(actions);
    inner.appendChild(footer);

    dropdown.appendChild(inner);
    this.appendChild(dropdown);
    this.dropdown = dropdown;

    /* ═══ STORE REFS ═══ */
    this.calendarLeft = calLeft;
    this.calendarRight = calRight;
    this.summaryEl = summary;
    this.applyBtn = applyBtn;
    this.cancelBtn = cancelBtn;

    this.syncPlacement();
  }

  /* ═══ MIRROR PLACEMENT TO PUBLIC DATA ATTRIBUTES ═══ */
  syncPlacement() {
    const side = normalizeDateRangeSide(this.getAttribute('placement'));

    this.setAttribute('data-side', side);
    if (this.dropdown) {
      this.dropdown.setAttribute('data-side', side);
    }
  }

  /* ═══ SET UP EVENT LISTENERS ═══ */
  setupEventListeners() {
    this.trigger.addEventListener('click', this.handleTriggerClick);
    this.trigger.addEventListener('keydown', this.handleTriggerKeyDown);
    document.addEventListener('click', this.handleDocumentClick);

    /* ═══ CALENDAR SELECT EVENTS ═══ */
    this.calendarLeft.addEventListener('ren-date-select', this.handleCalendarSelect);
    this.calendarRight.addEventListener('ren-date-select', this.handleCalendarSelect);

    /* ═══ FOOTER ACTIONS ═══ */
    this.applyBtn.addEventListener('click', this.handleApply);
    this.cancelBtn.addEventListener('click', this.handleCancel);

    /* ═══ SYNC CALENDAR MONTHS ═══ */
    this.syncCalendarMonths();
  }

  /* ═══ SYNC LEFT/RIGHT CALENDAR MONTHS ═══ */
  syncCalendarMonths() {
    if (this.calendarLeft && this.calendarLeft.goToMonth) {
      this.calendarLeft.goToMonth(
        this.leftMonth.getFullYear(),
        this.leftMonth.getMonth()
      );
    }

    if (this.calendarRight && this.calendarRight.goToMonth) {
      this.calendarRight.goToMonth(
        this.rightMonth.getFullYear(),
        this.rightMonth.getMonth()
      );
    }

    /* ═══ APPLY DRAFT RANGE TO BOTH CALENDARS ═══ */
    if (this.draftStart && this.draftEnd) {
      const startStr = this.dateToString(this.draftStart);
      const endStr = this.dateToString(this.draftEnd);

      if (this.calendarLeft.setRange) {
        this.calendarLeft.setRange(startStr, endStr);
      }
      if (this.calendarRight.setRange) {
        this.calendarRight.setRange(startStr, endStr);
      }
    }
  }

  /* ═══ ADD HIDDEN INPUTS FOR FORM SUBMISSION ═══ */
  addHiddenInputs() {
    const name = this.getAttribute('name') || 'date-range';

    let hiddenStart = this.querySelector('input[name$="-start"]');
    if (!hiddenStart) {
      hiddenStart = document.createElement('input');
      hiddenStart.type = 'hidden';
      hiddenStart.name = `${name}-start`;
      this.appendChild(hiddenStart);
    }

    let hiddenEnd = this.querySelector('input[name$="-end"]');
    if (!hiddenEnd) {
      hiddenEnd = document.createElement('input');
      hiddenEnd.type = 'hidden';
      hiddenEnd.name = `${name}-end`;
      this.appendChild(hiddenEnd);
    }

    this.hiddenStart = hiddenStart;
    this.hiddenEnd = hiddenEnd;
  }

  /* ═══ HANDLE TRIGGER CLICK ═══ */
  handleTriggerClick(event) {
    event.preventDefault();
    event.stopPropagation();
    if (this.hasAttribute('disabled')) return;
    this.isOpen ? this.close() : this.open();
  }

  /* ═══ HANDLE TRIGGER KEYBOARD ═══ */
  handleTriggerKeyDown(event) {
    if (this.hasAttribute('disabled')) return;

    if (event.key === 'Enter' || event.key === ' ') {
      event.preventDefault();
      this.open();
    }

    if (event.key === 'Escape') {
      this.close();
    }
  }

  /* ═══ HANDLE OUTSIDE CLICK ═══ */
  handleDocumentClick(event) {
    if (!this.contains(event.target) && this.isOpen) {
      this.handleCancel();
    }
  }

  /* ═══ HANDLE CALENDAR DATE SELECT ═══ */
  handleCalendarSelect(event) {
    const detail = event.detail;

    if (detail.range) {
      /* ═══ UPDATE DRAFT RANGE ═══ */
      if (detail.range.start) {
        this.draftStart = detail.range.start;
      }
      if (detail.range.end) {
        this.draftEnd = detail.range.end;
      }

      /* ═══ ENSURE START < END ═══ */
      if (this.draftStart && this.draftEnd && this.draftStart > this.draftEnd) {
        const tmp = this.draftStart;
        this.draftStart = this.draftEnd;
        this.draftEnd = tmp;
      }

      /* ═══ SYNC RANGE ACROSS BOTH CALENDARS ═══ */
      if (this.draftStart && this.draftEnd) {
        const startStr = this.dateToString(this.draftStart);
        const endStr = this.dateToString(this.draftEnd);

        [this.calendarLeft, this.calendarRight].forEach((cal) => {
          if (cal !== event.target && cal.setRange) {
            cal.setRange(startStr, endStr);
          }
        });
      }

      /* ═══ CLEAR ACTIVE PRESET ═══ */
      this.activePreset = null;
      this.updatePresetHighlight();

      /* ═══ UPDATE SUMMARY + APPLY BUTTON ═══ */
      this.updateSummary();
      this.updateApplyState();
    }
  }

  /* ═══ APPLY PRESET ═══ */
  applyPreset(key) {
    const preset = PRESETS[key];
    if (!preset) return;

    const range = preset.getRange();
    this.draftStart = range.start;
    this.draftEnd = range.end;
    this.activePreset = key;

    /* ═══ NAVIGATE CALENDARS TO SHOW THE RANGE ═══ */
    this.leftMonth = new Date(range.start.getFullYear(), range.start.getMonth());
    this.rightMonth = new Date(this.leftMonth.getFullYear(), this.leftMonth.getMonth() + 1);

    /* ═══ IF END IS IN A DIFFERENT MONTH, ADJUST ═══ */
    if (range.end.getMonth() !== range.start.getMonth() || range.end.getFullYear() !== range.start.getFullYear()) {
      this.rightMonth = new Date(range.end.getFullYear(), range.end.getMonth());
      /* ═══ ENSURE LEFT IS BEFORE RIGHT ═══ */
      if (this.rightMonth <= this.leftMonth) {
        this.leftMonth = new Date(this.rightMonth.getFullYear(), this.rightMonth.getMonth() - 1);
      }
    }

    this.syncCalendarMonths();
    this.updatePresetHighlight();
    this.updateSummary();
    this.updateApplyState();
  }

  /* ═══ UPDATE PRESET BUTTON HIGHLIGHT ═══ */
  updatePresetHighlight() {
    const presetBtns = this.querySelectorAll('.ren-date-range-preset');
    presetBtns.forEach((btn) => {
      const key = btn.getAttribute('data-preset');
      if (key === this.activePreset) {
        btn.setAttribute('aria-selected', 'true');
      } else {
        btn.removeAttribute('aria-selected');
      }
    });
  }

  /* ═══ UPDATE FOOTER SUMMARY ═══ */
  updateSummary() {
    if (!this.summaryEl) return;

    if (this.draftStart && this.draftEnd) {
      const start = this.formatDate(this.draftStart);
      const end = this.formatDate(this.draftEnd);
      const days = this.daysBetween(this.draftStart, this.draftEnd);
      this.summaryEl.innerHTML = `<strong>${start}</strong> → <strong>${end}</strong> (${days} day${days !== 1 ? 's' : ''})`;
    } else if (this.draftStart) {
      this.summaryEl.innerHTML = `<strong>${this.formatDate(this.draftStart)}</strong> → ...`;
    } else {
      this.summaryEl.textContent = 'Select a start date';
    }
  }

  /* ═══ UPDATE APPLY BUTTON STATE ═══ */
  updateApplyState() {
    if (this.applyBtn) {
      this.applyBtn.disabled = !(this.draftStart && this.draftEnd);
    }
  }

  /* ═══ HANDLE APPLY ═══ */
  handleApply() {
    if (!this.draftStart || !this.draftEnd) return;

    this.confirmedStart = new Date(this.draftStart);
    this.confirmedEnd = new Date(this.draftEnd);

    this.updateTriggerDisplay();
    this.updateHiddenInputs();
    this.dispatchChangeEvent();
    this.close();
  }

  /* ═══ HANDLE CANCEL ═══ */
  handleCancel() {
    /* ═══ REVERT DRAFT TO CONFIRMED ═══ */
    this.draftStart = this.confirmedStart ? new Date(this.confirmedStart) : null;
    this.draftEnd = this.confirmedEnd ? new Date(this.confirmedEnd) : null;
    this.activePreset = null;
    this.close();
  }

  /* ═══ OPEN DROPDOWN ═══ */
  open() {
    if (this.isOpen || this.hasAttribute('disabled')) return;
    if (!this.dropdown) return;

    /* ═══ RESET DRAFT TO CONFIRMED ═══ */
    this.draftStart = this.confirmedStart ? new Date(this.confirmedStart) : null;
    this.draftEnd = this.confirmedEnd ? new Date(this.confirmedEnd) : null;

    /* ═══ SET CALENDAR MONTHS ═══ */
    if (this.confirmedStart) {
      this.leftMonth = new Date(this.confirmedStart.getFullYear(), this.confirmedStart.getMonth());
      this.rightMonth = new Date(this.leftMonth.getFullYear(), this.leftMonth.getMonth() + 1);
    }

    this.syncPlacement();

    try {
      this.dropdown.showPopover();
    } catch {
      this.dropdown.style.display = 'block';
    }

    this.isOpen = true;

    if (this.trigger) {
      this.trigger.setAttribute('aria-expanded', 'true');
    }

    this.syncCalendarMonths();
    this.updateSummary();
    this.updateApplyState();
    this.updatePresetHighlight();

    /* ═══ FOCUS FIRST PRESET OR CALENDAR ═══ */
    setTimeout(() => {
      const firstPreset = this.querySelector('.ren-date-range-preset');
      if (firstPreset) {
        firstPreset.focus();
      } else {
        const firstDay = this.calendarLeft?.querySelector('.ren-calendar-day:not([disabled])');
        if (firstDay) firstDay.focus();
      }
    }, 100);

    this.dispatchEvent(
      new CustomEvent('ren-date-range-open', {
        bubbles: true,
        composed: true,
      })
    );
  }

  /* ═══ CLOSE DROPDOWN ═══ */
  close() {
    if (!this.isOpen) return;
    if (!this.dropdown) return;

    try {
      this.dropdown.hidePopover();
    } catch {
      this.dropdown.style.display = 'none';
    }

    this.isOpen = false;

    if (this.trigger) {
      this.trigger.setAttribute('aria-expanded', 'false');
      this.trigger.focus();
    }

    this.dispatchEvent(
      new CustomEvent('ren-date-range-close', {
        bubbles: true,
        composed: true,
      })
    );
  }

  /* ═══ UPDATE TRIGGER DISPLAY ═══ */
  updateTriggerDisplay() {
    if (!this.trigger) return;

    if (this.confirmedStart && this.confirmedEnd) {
      const startEl = this.trigger.querySelector('.ren-date-range-start');
      const endEl = this.trigger.querySelector('.ren-date-range-end');

      if (startEl) startEl.textContent = this.formatDate(this.confirmedStart);
      if (endEl) endEl.textContent = this.formatDate(this.confirmedEnd);

      this.trigger.classList.remove('ren-date-range-empty');
    } else {
      this.trigger.classList.add('ren-date-range-empty');
    }
  }

  /* ═══ UPDATE HIDDEN INPUTS ═══ */
  updateHiddenInputs() {
    if (this.hiddenStart) {
      this.hiddenStart.value = this.confirmedStart ? this.dateToString(this.confirmedStart) : '';
    }
    if (this.hiddenEnd) {
      this.hiddenEnd.value = this.confirmedEnd ? this.dateToString(this.confirmedEnd) : '';
    }
  }

  /* ═══ DISPATCH CHANGE EVENT ═══ */
  dispatchChangeEvent() {
    const value =
      this.confirmedStart && this.confirmedEnd
        ? {
            start: this.dateToString(this.confirmedStart),
            end: this.dateToString(this.confirmedEnd),
          }
        : null;

    const formattedValue =
      this.confirmedStart && this.confirmedEnd
        ? `${this.formatDate(this.confirmedStart)} → ${this.formatDate(this.confirmedEnd)}`
        : '';

    this.dispatchEvent(
      new CustomEvent('ren-change', {
        detail: {
          value,
          formattedValue,
          preset: this.activePreset || null,
          days: this.confirmedStart && this.confirmedEnd
            ? this.daysBetween(this.confirmedStart, this.confirmedEnd)
            : 0,
        },
        bubbles: true,
        composed: true,
      })
    );
  }

  /* ═══ FORMAT DATE ═══ */
  formatDate(date) {
    if (!date) return '';
    const formatter = new Intl.DateTimeFormat(this.locale, {
      year: 'numeric',
      month: this.format === 'short' ? '2-digit' : this.format === 'long' ? 'long' : 'short',
      day: '2-digit',
    });
    return formatter.format(date);
  }

  /* ═══ DATE TO ISO STRING ═══ */
  dateToString(date) {
    const year = date.getFullYear();
    const month = String(date.getMonth() + 1).padStart(2, '0');
    const day = String(date.getDate()).padStart(2, '0');
    return `${year}-${month}-${day}`;
  }

  parseDate(value) {
    if (value instanceof Date) return new Date(value.getTime());
    const match = String(value).match(/^(\d{4})-(\d{2})-(\d{2})$/);
    return match ? new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3])) : new Date(value);
  }

  /* ═══ DAYS BETWEEN TWO DATES ═══ */
  daysBetween(start, end) {
    const MS_PER_DAY = 1000 * 60 * 60 * 24;
    const diff = Math.abs(end.getTime() - start.getTime());
    return Math.round(diff / MS_PER_DAY) + 1;
  }

  /* ═══ PUBLIC API ═══ */

  /* ═══ GET VALUE ═══ */
  getValue() {
    if (!this.confirmedStart || !this.confirmedEnd) return null;
    return {
      start: this.dateToString(this.confirmedStart),
      end: this.dateToString(this.confirmedEnd),
    };
  }

  /* ═══ SET VALUE ═══ */
  setValue(start, end) {
    this.confirmedStart = this.parseDate(start);
    this.confirmedEnd = this.parseDate(end);
    this.draftStart = this.parseDate(start);
    this.draftEnd = this.parseDate(end);
    this.updateTriggerDisplay();
    this.updateHiddenInputs();
  }

  /* ═══ CLEAR VALUE ═══ */
  clear() {
    this.confirmedStart = null;
    this.confirmedEnd = null;
    this.draftStart = null;
    this.draftEnd = null;
    this.activePreset = null;
    this.updateTriggerDisplay();
    this.updateHiddenInputs();
  }

  /* ═══ GETTERS/SETTERS ═══ */
  get disabled() {
    return this.hasAttribute('disabled');
  }

  set disabled(val) {
    if (val) {
      this.setAttribute('disabled', '');
      if (this.trigger) this.trigger.disabled = true;
    } else {
      this.removeAttribute('disabled');
      if (this.trigger) this.trigger.disabled = false;
    }
  }

  get value() {
    return this.getValue();
  }

  set value(val) {
    if (val && val.start && val.end) {
      this.setValue(val.start, val.end);
    } else {
      this.clear();
    }
  }
}

/* ═══ REGISTER COMPONENT ═══ */
if (!customElements.get('ren-date-range-picker')) {
  customElements.define('ren-date-range-picker', RenDateRangePicker);
}


id: file:components/composites/ren-dialog/ren-dialog.css
type: css
name: ren-dialog.css
path: components/composites/ren-dialog/ren-dialog.css
data: {}
body:
/* ═══════════════════════════════════════════════════════════════════════════
   REN DIALOG / MODAL COMPONENT
   Native <dialog> element enhancement with CSS animations and modern APIs
   Uses @starting-style, transition-behavior: allow-discrete, and CSS nesting
   ═══════════════════════════════════════════════════════════════════════════ */

/* ═══ DIALOG CONTAINER ═══ */
.ren-dialog {
  /* Structure */
  margin: auto;
  padding: 0;
  border: none;

  /* Sizing */
  max-width: var(--dialog-max-width, 32rem);
  max-height: 85dvh;
  width: 90vw;
  overflow: auto;

  /* Appearance */
  background: var(--color-surface);
  color: var(--color-text);
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-xl);

  /* Smooth scrolling for body overflow */
  scrollbar-gutter: stable;

  /* ═══ ANIMATION STATES ═══
     Uses semantic motion tokens so global motion tuning
     cascades to every component. See tokens/semantic/motion.css. */
  opacity: 1;
  transform: scale(1) translateY(0);

  transition:
    opacity   var(--duration-enter) var(--ease-enter),
    transform var(--duration-enter) var(--ease-enter),
    overlay   var(--duration-enter) allow-discrete,
    display   var(--duration-enter) allow-discrete;

  /* ═══ ENTRY ANIMATION (CSS Transitions instead of keyframes) ═══ */
  @starting-style {
    opacity: 0;
    transform: scale(0.95) translateY(8px);
  }

  /* ═══ BACKDROP STYLING ═══
     Uses --transition-overlay so scrim fade/blur duration
     is tuned in one place (tokens/semantic/motion.css). */
  &::backdrop {
    background-color: rgb(0, 0, 0, 0);
    backdrop-filter: blur(0px);

    transition: var(--transition-overlay);

    /* ═══ BACKDROP ENTRY ANIMATION ═══ */
    @starting-style {
      background-color: rgb(0, 0, 0, 0);
      backdrop-filter: blur(0px);
    }
  }

  /* ═══ OPEN STATE ═══ */
  &[open] {
    opacity: 1;
    transform: scale(1) translateY(0);

    &::backdrop {
      background-color: var(--color-overlay);
      backdrop-filter: blur(4px);
    }
  }

  /* ═══ REDUCED MOTION ═══ */
  @media (prefers-reduced-motion: reduce) {
    transition: none;

    &::backdrop {
      transition: none;
    }
  }
}

/* ═══ DIALOG HEADER ═══
   No bottom border — separation from the body comes from spacing, not lines.
   Header padding-bottom + body padding-top give the breathing room. */
.ren-dialog-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-6) var(--space-6) var(--space-3);
  flex-shrink: 0;
}

/* ═══ DIALOG TITLE ═══ */
.ren-dialog-title {
  margin: 0;
  font-size: 1.25rem; /* ~20px */
  line-height: 1.5;
  font-weight: 600;
  color: var(--color-text);
}

/* ═══ DIALOG DESCRIPTION ═══ */
.ren-dialog-description {
  margin: 0;
  margin-top: var(--space-2);
  font-size: 0.875rem; /* caption */
  line-height: 1.5;
  color: var(--color-text-muted);
}

/* ═══ DIALOG BODY ═══ */
.ren-dialog-body {
  padding: var(--space-6);
  overflow-y: auto;
  flex: 1;
  min-height: 0; /* Allow flex shrinking */

  /* ═══ SCROLLBAR STYLING ═══ */
  &::-webkit-scrollbar {
    width: 8px;
  }

  &::-webkit-scrollbar-track {
    background: transparent;
  }

  &::-webkit-scrollbar-thumb {
    background: var(--color-border);
    border-radius: 4px;

    &:hover {
      background: var(--color-border-hover);
    }
  }
}

/* ═══ DIALOG FOOTER ═══ */
.ren-dialog-footer {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-3);
  padding: var(--space-6);
  border-top: 1px solid var(--color-border);
  flex-shrink: 0;
}

/* ═══ CLOSE BUTTON ═══ */
.ren-dialog-close {
  margin-inline-start: auto;
  padding: var(--space-2);
  background: transparent;
  border: none;
  cursor: pointer;
  color: var(--color-text-muted);
  border-radius: var(--radius-md);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1;

  /* Tactile transitions (hover / focus / press) */
  transition: var(--transition-tactile);

  &:hover {
    background-color: var(--color-background-hover);
    color: var(--color-text);
  }

  &:active {
    background-color: var(--color-background-active);
  }

  &:focus {
    outline: var(--ring-width) solid var(--color-focus-ring);
    outline-offset: var(--ring-offset-width);
  }

  & svg {
    width: 1.25rem;
    height: 1.25rem;
  }
}

/* ═══ SIZE VARIANTS ═══ */

/* Small dialog (24rem) */
.ren-dialog-sm {
  --dialog-max-width: 24rem;
}

/* Medium dialog (32rem) - DEFAULT */
.ren-dialog-md {
  --dialog-max-width: 32rem;
}

/* Large dialog (42rem) */
.ren-dialog-lg {
  --dialog-max-width: 42rem;
}

/* Extra Large dialog (56rem) */
.ren-dialog-xl {
  --dialog-max-width: 56rem;
}

/* Full width dialog (95dvw) */
.ren-dialog-full {
  max-width: 95dvw;
  max-height: 95dvh;
  width: 95dvw;
}

/* ═══ ALERT DIALOG VARIANT ═══ */
.ren-alert-dialog {
  /* No additional styling needed - alert behavior handled in JS */
}

/* ═══ MOBILE RESPONSIVE ═══ */
@media (max-width: 640px) {
  .ren-dialog {
    max-width: 100%;
    max-height: 100dvh;
    width: 100%;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;

    /* ═══ BOTTOM SHEET ANIMATION (Mobile) ═══ */
    &[data-mobile-sheet] {
      position: fixed;
      bottom: 0;
      left: 0;
      right: 0;
      margin: 0;
      border-radius: var(--radius-lg) var(--radius-lg) 0 0;

      transform: translateY(100%);
      transition:
        transform var(--duration-enter) var(--ease-enter),
        opacity   var(--duration-enter) var(--ease-enter),
        overlay   var(--duration-enter) allow-discrete,
        display   var(--duration-enter) allow-discrete;

      @starting-style {
        transform: translateY(100%);
      }

      &[open] {
        transform: translateY(0);
      }
    }
  }

  .ren-dialog-header,
  .ren-dialog-body,
  .ren-dialog-footer {
    padding: var(--space-4);
  }

  .ren-dialog-title {
    font-size: 1.125rem;
  }
}

/* ═══ WEB COMPONENT CONTAINER ═══ */
ren-dialog {
  display: contents;
}

.ren-dialog-wrapper {
  display: flex;
  flex-direction: column;
  height: 100%;
}
/* Appearance API bridge: public component tokens remain overridable at any scope. */
.ren-dialog,
.ren-alert-dialog {
  width: var(--ren-dialog-width, min(90vw, 500px));
  padding: var(--ren-dialog-padding, var(--space-6));
  border-radius: var(--ren-dialog-radius, var(--radius-xl));
  background: var(--ren-dialog-bg, var(--color-surface-overlay));
  border-color: var(--ren-dialog-border-color, var(--color-border));
  box-shadow: var(--ren-dialog-shadow, var(--shadow-xl));
  gap: var(--ren-dialog-gap, var(--space-4));
  transition-duration: var(--ren-dialog-duration, var(--duration-normal));
  transition-timing-function: var(--ren-dialog-easing, var(--ease-out));
}
.ren-dialog::backdrop,
.ren-alert-dialog::backdrop { background: var(--ren-dialog-backdrop, var(--color-overlay)); }


id: file:components/composites/ren-dialog/ren-dialog.js
type: javascript
name: ren-dialog.js
path: components/composites/ren-dialog/ren-dialog.js
data: {}
body:
import { createFocusTrap } from '../../../utils/focus-trap.js';

/**
 * RenDialog - Modal/Dialog Web Component
 *
 * A minimal, modern dialog component built on the native <dialog> element.
 * Animations are handled entirely by CSS using @starting-style and transitions.
 *
 * Features:
 * - Native <dialog> element with `.ren-dialog` enhancement
 * - Auto-wiring of [data-dialog-trigger] and [data-dialog-close] buttons
 * - Progressive enhancement: [commandfor] and [command] attributes support
 * - Progressive enhancement: closedby attribute for modern browsers
 * - Focus trap management for accessibility
 * - Escape key support and click-outside dismissal via native dialog + simple click handler
 * - Size variants (sm, md, lg, xl, full)
 * - Mobile-responsive bottom sheet behavior
 * - Custom events: ren-open, ren-close
 *
 * @example
 * <ren-dialog size="md">
 *   <dialog>
 *     <div class="ren-dialog-header">
 *       <h2 class="ren-dialog-title">Confirm Action</h2>
 *       <button class="ren-dialog-close" data-dialog-close>&times;</button>
 *     </div>
 *     <div class="ren-dialog-body">Are you sure?</div>
 *     <div class="ren-dialog-footer">
 *       <button data-dialog-close>Cancel</button>
 *       <button>Confirm</button>
 *     </div>
 *   </dialog>
 * </ren-dialog>
 *
 * @example
 * <button data-dialog-trigger="my-dialog">Open</button>
 * <ren-dialog id="my-dialog">...</ren-dialog>
 *
 * @example
 * <button commandfor="my-dialog" command="show-modal">Open</button>
 * <ren-dialog id="my-dialog">...</ren-dialog>
 */
export class RenDialog extends HTMLElement {
  #dialogElement = null;
  #focusTrap = null;
  #abortController = null;
  #returnFocus = null;
  #inertElements = new WeakMap();
  #closeReconciled = true;

  static observedAttributes = ['open', 'alert', 'size'];

  constructor() {
    super();
    this.#setupDialogElement();
  }

  connectedCallback() {
    if (!this.#abortController || this.#abortController.signal.aborted) {
      this.#abortController = new AbortController();
    }
    this.#initializeDialogElement();
    this.#wireupTriggers();
  }

  disconnectedCallback() {
    this.#abortController?.abort();
    if (this.#dialogElement?.open) this.#setInert(false);
    this.#teardownFocusTrap();
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue === newValue) return;

    switch (name) {
      case 'open':
        newValue !== null ? this.show() : this.close();
        break;
      case 'size':
        this.#updateSizeClass();
        break;
    }
  }

  /* ═══ PUBLIC API ═══ */

  /**
   * Show the dialog with modal behavior
   * @public
   */
  show() {
    if (!this.#dialogElement?.open) {
      this.#returnFocus = document.activeElement;
      this.#closeReconciled = false;
      this.#dialogElement.showModal();
      this.#setInert(true);
      this.setAttribute('open', '');
      this.setAttribute('data-state', 'open');
      this.#setupFocusTrap();
      this.#updateMobileSheet();
      this.dispatchEvent(new CustomEvent('ren-open', { bubbles: true, composed: true }));
    }
  }

  /**
   * Alias for show()
   * @public
   */
  open() {
    this.show();
  }

  /**
   * Close the dialog
   * CSS transitions handle the exit animation automatically
   *
   * @public
   * @param {string} [returnValue]
   */
  close(returnValue = '') {
    if (this.#dialogElement?.open) {
      this.#dialogElement.close(returnValue);
    }
  }

  /**
   * Check if dialog is open
   * @public
   * @returns {boolean}
   */
  get isOpen() {
    return this.#dialogElement?.open ?? false;
  }

  /**
   * Get the underlying dialog element
   * @public
   * @returns {HTMLDialogElement | null}
   */
  get dialog() {
    return this.#dialogElement;
  }

  /* ═══ PRIVATE: INITIALIZATION ═══ */

  /**
   * Find or create the dialog element
   * @private
   */
  #setupDialogElement() {
    this.#dialogElement = this.querySelector('dialog');
    if (!this.#dialogElement) {
      this.#dialogElement = document.createElement('dialog');
      this.appendChild(this.#dialogElement);
    }
  }

  /**
   * Initialize dialog with classes and event listeners
   * @private
   */
  #initializeDialogElement() {
    if (!this.#dialogElement) this.#setupDialogElement();

    this.#dialogElement.classList.add('ren-dialog');
    this.#updateSizeClass();

    if (this.hasAttribute('alert')) {
      this.#dialogElement.classList.add('ren-alert-dialog');
      this.#dialogElement.setAttribute('role', 'alertdialog');
    }

    // Add aria-label fallback if no title found
    if (!this.#dialogElement.getAttribute('aria-label')) {
      const title = this.#dialogElement.querySelector('.ren-dialog-title, [role="heading"]');
      if (title) {
        this.#dialogElement.setAttribute('aria-label', title.textContent?.trim() || 'Dialog');
      } else {
        this.#dialogElement.setAttribute('aria-label', 'Dialog');
      }
    }

    // Block native Escape dismissal only for non-dismissable variants.
    this.#dialogElement.addEventListener(
      'cancel',
      (e) => {
        if (this.hasAttribute('no-escape') || this.hasAttribute('alert')) {
          e.preventDefault();
        }
      },
      { signal: this.#abortController.signal }
    );

    // Native Escape and <form method="dialog"> both land here. All close
    // paths reconcile through this single listener so lifecycle state and the
    // public event cannot diverge or fire twice.
    this.#dialogElement.addEventListener(
      'close',
      () => this.#reconcileNativeClose(),
      { signal: this.#abortController.signal }
    );

    // Handle click-outside dismissal
    // Clicking on the dialog itself when event.target === dialog means backdrop was clicked
    this.#dialogElement.addEventListener(
      'click',
      (e) => {
        if (e.target === this.#dialogElement && !this.hasAttribute('alert')) {
          this.close();
        }
      },
      { signal: this.#abortController.signal }
    );

    // Wire up close buttons
    this.#wireupCloseButtons();

    // Progressive enhancement: set closedby attribute for modern browsers
    if ('closedBy' in HTMLDialogElement.prototype) {
      if (this.hasAttribute('alert')) {
        this.#dialogElement.closedBy = 'closerequest';
      } else if (this.hasAttribute('no-escape')) {
        this.#dialogElement.closedBy = 'none';
      } else {
        this.#dialogElement.closedBy = 'any';
      }
    }
  }

  /**
   * Wire up trigger buttons that open this dialog
   * Supports both [data-dialog-trigger] and native commandfor/command attributes
   * @private
   */
  #wireupTriggers() {
    const id = this.id;
    if (!id) return;

    const triggers = document.querySelectorAll(`[data-dialog-trigger="${id}"]`);
    triggers.forEach((trigger) => {
      trigger.addEventListener(
        'click',
        (e) => {
          e.preventDefault();
          this.show();
        },
        { signal: this.#abortController.signal }
      );
    });

    // Progressive enhancement: support command/commandfor in browsers that don't yet support it natively
    if (!('commandForElement' in HTMLButtonElement.prototype)) {
      const commandTriggers = document.querySelectorAll(`[commandfor="${id}"]`);
      commandTriggers.forEach((trigger) => {
        const command = trigger.getAttribute('command');
        if (command === 'show-modal' || command === 'show') {
          trigger.addEventListener(
            'click',
            (e) => {
              e.preventDefault();
              this.show();
            },
            { signal: this.#abortController.signal }
          );
        } else if (command === 'close') {
          trigger.addEventListener(
            'click',
            (e) => {
              e.preventDefault();
              this.close();
            },
            { signal: this.#abortController.signal }
          );
        }
      });
    }
  }

  /**
   * Wire up close buttons within the dialog
   * @private
   */
  #wireupCloseButtons() {
    const closeButtons = this.#dialogElement?.querySelectorAll('[data-dialog-close]');
    closeButtons?.forEach((button) => {
      button.addEventListener(
        'click',
        (e) => {
          e.preventDefault();
          // [data-dialog-close="value"] propagates `value` as ren-close.detail.returnValue.
          // Bare [data-dialog-close] (no value) closes with "".
          const returnValue = button.getAttribute('data-dialog-close') ?? '';
          this.close(returnValue);
        },
        { signal: this.#abortController.signal }
      );
    });
  }

  /* ═══ PRIVATE: FOCUS MANAGEMENT ═══ */

  /**
   * Setup focus trap for keyboard navigation
   * @private
   */
  #setupFocusTrap() {
    if (this.#focusTrap) return;
    this.#focusTrap = createFocusTrap(this.#dialogElement, {
      initialFocus: this.#dialogElement.querySelector('[autofocus]'),
      returnFocus: this.#returnFocus,
    });
    this.#focusTrap.activate();
  }

  /**
   * Teardown focus trap
   * @private
   */
  #teardownFocusTrap() {
    if (this.#focusTrap) {
      this.#focusTrap.deactivate();
      this.#focusTrap = null;
    }
  }

  #reconcileNativeClose() {
    if (this.#closeReconciled) return;
    this.#closeReconciled = true;
    const returnValue = this.#dialogElement?.returnValue || '';

    this.#setInert(false);
    this.removeAttribute('open');
    this.setAttribute('data-state', 'closed');
    this.#teardownFocusTrap();
    if (this.#returnFocus && document.contains(this.#returnFocus)) {
      this.#returnFocus.focus({ preventScroll: true });
    }
    this.#returnFocus = null;
    this.dispatchEvent(
      new CustomEvent('ren-close', {
        bubbles: true,
        composed: true,
        detail: { returnValue },
      })
    );
  }

  /* ═══ PRIVATE: UTILITIES ═══ */

  /**
   * Update size variant class
   * @private
   */
  #updateSizeClass() {
    if (!this.#dialogElement) return;
    this.#dialogElement.classList.remove(
      'ren-dialog-sm',
      'ren-dialog-md',
      'ren-dialog-lg',
      'ren-dialog-xl',
      'ren-dialog-full'
    );
    const size = this.getAttribute('size') || 'md';
    this.#dialogElement.classList.add(`ren-dialog-${size}`);
  }

  /**
   * Apply mobile bottom sheet styling on small screens
   * @private
   */
  #updateMobileSheet() {
    if (!this.#dialogElement) return;
    if (window.innerWidth <= 640) {
      this.#dialogElement.setAttribute('data-mobile-sheet', '');
    } else {
      this.#dialogElement.removeAttribute('data-mobile-sheet');
    }
  }

  /**
   * Toggle inert attribute on sibling elements
   * When active, makes background elements non-interactive
   * @private
   * @param {boolean} active
   */
  #setInert(active) {
    const children = document.body.children;
    for (let i = 0; i < children.length; i++) {
      const child = children[i];

      // Skip the dialog itself and meta elements
      if (
        child === this ||
        child.tagName === 'SCRIPT' ||
        child.tagName === 'STYLE' ||
        child.tagName === 'LINK'
      ) {
        continue;
      }

      if (active) {
        // Save original inert state and set inert
        if (!this.#inertElements.has(child)) {
          this.#inertElements.set(child, child.inert);
        }
        child.inert = true;
      } else {
        // Restore original inert state
        const originalInert = this.#inertElements.get(child);
        if (originalInert !== undefined) {
          child.inert = originalInert;
          this.#inertElements.delete(child);
        }
      }
    }
  }
}

/**
 * RenAlertDialog - Alert Dialog Variant
 *
 * An alert dialog that prevents click-outside dismissal by default.
 * Escape key and other behavior can be controlled via attributes.
 *
 * @example
 * <ren-alert-dialog>
 *   <dialog>...</dialog>
 * </ren-alert-dialog>
 */
export class RenAlertDialog extends RenDialog {
  constructor() {
    super();
    this.setAttribute('alert', '');
  }
}

/* ═══ REGISTER CUSTOM ELEMENTS ═══ */
if (!customElements.get('ren-dialog')) {
  customElements.define('ren-dialog', RenDialog);
}
if (!customElements.get('ren-alert-dialog')) {
  customElements.define('ren-alert-dialog', RenAlertDialog);
}


id: file:components/composites/ren-dropzone/ren-dropzone.css
type: css
name: ren-dropzone.css
path: components/composites/ren-dropzone/ren-dropzone.css
data: {}
body:
/* ============================================
   RenDS — DropZone Component
   ============================================
   Drag & drop file upload area.
   Minimal JS for drag events — CSS handles visuals.

   Uses native drag events, no library needed.

   Usage:
     <div class="ren-dropzone" data-accept=".jpg,.png,.pdf" data-max-size="5MB">
       <div class="ren-dropzone-content">
         <span class="ren-dropzone-icon">↑</span>
         <p class="ren-dropzone-title">Drop files here</p>
         <p class="ren-dropzone-description">or click to browse</p>
       </div>
       <input type="file" class="ren-dropzone-input" multiple accept=".jpg,.png,.pdf">
     </div>
   ============================================ */

.ren-dropzone {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 10rem;
  padding: var(--space-6);
  border: 2px dashed var(--color-border-strong);
  border-radius: var(--radius-lg);
  background-color: var(--color-surface-sunken);
  cursor: pointer;
  transition: var(--transition-tactile);
}

.ren-dropzone:hover {
  border-color: var(--color-accent);
  background-color: var(--color-accent-subtle);
}

/* Drag over state (set via JS: element.dataset.dragover = true) */
.ren-dropzone[data-dragover] {
  border-color: var(--color-accent);
  background-color: var(--color-accent-subtle);
  border-style: solid;
}

.ren-dropzone:focus-within {
  outline: var(--ring-width) solid var(--color-focus-ring);
  outline-offset: var(--ring-offset-width);
}

/* Hidden file input covers entire dropzone */
.ren-dropzone-input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  cursor: pointer;
}

/* Content */
.ren-dropzone-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  text-align: center;
  pointer-events: none;
}

.ren-dropzone-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 3rem;
  height: 3rem;
  font-size: var(--text-xl);
  color: var(--color-text-muted);
  background-color: var(--color-fill);
  border-radius: var(--radius-full);
  margin-bottom: var(--space-1);
}

.ren-dropzone[data-dragover] .ren-dropzone-icon {
  color: var(--color-accent);
  background-color: color-mix(in oklch, var(--color-accent), transparent 85%);
}

.ren-dropzone-title {
  font-size: var(--body-size);
  font-weight: var(--weight-medium);
  color: var(--color-text);
}

.ren-dropzone-description {
  font-size: var(--caption-size, var(--text-sm));
  color: var(--color-text-muted);
}

/* ─── File list (after upload) ─── */
.ren-dropzone-files {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  width: 100%;
  margin-top: var(--space-3);
}

.ren-dropzone-file {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  background-color: var(--color-surface-raised);
  border: var(--stroke-1) solid var(--color-border);
  border-radius: var(--radius-md);
  font-size: var(--caption-size, var(--text-sm));
}

.ren-dropzone-file-name {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ren-dropzone-file-size {
  color: var(--color-text-muted);
  white-space: nowrap;
}

.ren-dropzone-file-remove {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.25rem;
  height: 1.25rem;
  padding: 0;
  border: none;
  background: none;
  color: var(--color-text-muted);
  cursor: pointer;
  border-radius: var(--radius-sm);
  font-size: var(--text-base);
}

.ren-dropzone-file-remove:hover {
  color: var(--color-danger);
}

/* ─── Compact variant ─── */
.ren-dropzone-compact {
  min-height: auto;
  padding: var(--space-3) var(--space-4);
  flex-direction: row;
  gap: var(--space-3);
}

.ren-dropzone-compact .ren-dropzone-content {
  flex-direction: row;
}

.ren-dropzone-compact .ren-dropzone-icon {
  width: 2rem;
  height: 2rem;
  font-size: var(--text-base);
  margin-bottom: 0;
}

/* ─── Disabled ─── */
.ren-dropzone[aria-disabled="true"],
.ren-dropzone:has(input:disabled) {
  opacity: 0.5;
  cursor: not-allowed;
  pointer-events: none;
}

/* ─── Error state ─── */
.ren-dropzone[data-error] {
  border-color: var(--color-danger);
}
/* Appearance API bridge */
.ren-dropzone { padding: var(--ren-dropzone-padding, var(--space-8)); border-radius: var(--ren-dropzone-radius, var(--radius-lg)); border-color: var(--ren-dropzone-border-color, var(--color-border-strong)); border-style: var(--ren-dropzone-border-style, dashed); border-width: var(--ren-dropzone-border-width, 2px); background: var(--ren-dropzone-bg, transparent); }
.ren-dropzone:hover, .ren-dropzone[data-drag-over] { background: var(--ren-dropzone-hover-bg, var(--color-accent-subtle)); border-color: var(--ren-dropzone-hover-border, var(--color-accent)); }


id: file:components/composites/ren-dropzone/ren-dropzone.js
type: javascript
name: ren-dropzone.js
path: components/composites/ren-dropzone/ren-dropzone.js
data: {}
body:
/* ============================================
   RenDS — DropZone Drag & Drop Handler
   ============================================
   Minimal JS for drag events. Handles:
   - Drag enter/leave visual state
   - File drop processing
   - Custom event dispatch

   Usage:
     import { initDropZone } from './ren-dropzone.js';
     const dz = initDropZone(document.querySelector('.ren-dropzone'));
     dz.addEventListener('ren-files-added', (e) => {
       console.log(e.detail.files);
     });
   ============================================ */

/**
 * @param {HTMLElement} dropzone
 * @returns {HTMLElement}
 */
const dropzoneControllers = new WeakMap();

export function initDropZone(dropzone) {
  if (!dropzone) return null;

  dropzoneControllers.get(dropzone)?.abort();
  delete dropzone.dataset.dragover;
  const controller = new AbortController();
  dropzoneControllers.set(dropzone, controller);
  const { signal } = controller;

  let dragCounter = 0;
  const input = dropzone.querySelector('.ren-dropzone-input');

  const handleFiles = (files) => {
    dropzone.dispatchEvent(
      new CustomEvent('ren-files-added', {
        detail: { files: [...files] },
        bubbles: true,
      })
    );
  };

  dropzone.addEventListener('dragenter', (e) => {
    e.preventDefault();
    dragCounter++;
    dropzone.dataset.dragover = '';
  }, { signal });

  dropzone.addEventListener('dragleave', () => {
    dragCounter--;
    if (dragCounter <= 0) {
      dragCounter = 0;
      delete dropzone.dataset.dragover;
    }
  }, { signal });

  dropzone.addEventListener('dragover', (e) => {
    e.preventDefault();
  }, { signal });

  dropzone.addEventListener('drop', (e) => {
    e.preventDefault();
    dragCounter = 0;
    delete dropzone.dataset.dragover;
    if (e.dataTransfer?.files?.length) {
      handleFiles(e.dataTransfer.files);
    }
  }, { signal });

  if (input) {
    input.addEventListener('change', () => {
      if (input.files?.length) {
        handleFiles(input.files);
      }
    }, { signal });
  }

  return dropzone;
}


id: file:components/composites/ren-hover-card/ren-hover-card.css
type: css
name: ren-hover-card.css
path: components/composites/ren-hover-card/ren-hover-card.css
data: {}
body:
/* ═══ REN HOVER CARD COMPONENT ═══ */

/* ═══ TRIGGER ELEMENT ═══ */
.ren-hover-card-trigger {
  anchor-name: --ren-hover-card-anchor;
}

/* ═══ BASE CARD ═══ */
.ren-hover-card {
  position-anchor: --ren-hover-card-anchor;
  position: absolute;
  position-area: bottom span-all;
  inset: auto;
  margin-block-start: var(--space-2);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-xl);
  padding: var(--space-4);
  max-width: 20rem;
  min-width: 15rem;
  z-index: 1000;

  /* ═══ POPOVER API ═══
     Semantic tokens keep hover-card in sync with tooltip + popover. */
  &:popover-open {
    animation: ren-hover-card-in var(--duration-enter) var(--ease-enter) both;
  }

  /* ═══ ARROW STYLING ═══ */
  &::before {
    content: '';
    position: absolute;
    width: 8px;
    height: 8px;
    background: inherit;
    border: inherit;
    border-inline-end: none;
    border-bottom: none;
    transform: rotate(45deg);
    z-index: -1;
    top: -4px;
    left: 50%;
    margin-inline-start: -4px;
  }

  /* ═══ FALLBACK POSITIONING ═══ */
  @supports not ((anchor-name: --ren-anchor) and
    (position-anchor: --ren-anchor) and
    (position-area: bottom span-all)) {
    position: fixed;
    top: auto;
    left: auto;
  }

  /* ═══ VIEWPORT EDGE HANDLING ═══ */
  @supports (position-try-fallbacks: flip-block) {
    position-try-fallbacks: flip-block;
  }
}

/* ═══ ANIMATION ═══ */
@starting-style {
  .ren-hover-card:popover-open {
    opacity: 0;
    translate: 0 -4px;
  }
}

@keyframes ren-hover-card-in {
  from {
    opacity: 0;
    translate: 0 -4px;
  }

  to {
    opacity: 1;
    translate: 0 0;
  }
}

/* ═══ SECTIONS ═══ */
.ren-hover-card-header {
  margin-bottom: var(--space-3);

  & h3,
  & h4 {
    margin: 0 0 var(--space-0-5) 0;
    font-size: var(--font-size-body);
    font-weight: 600;
    color: var(--color-text);
  }

  & p {
    margin: 0;
    font-size: var(--font-size-xs);
    color: var(--color-text-muted);
  }
}

.ren-hover-card-body {
  margin-bottom: var(--space-3);
  font-size: var(--font-size-body-sm);
  color: var(--color-text);
  line-height: 1.5;

  & :last-child {
    margin-bottom: 0;
  }
}

.ren-hover-card-footer {
  display: flex;
  gap: var(--space-2);
  align-items: center;

  & button,
  & a {
    font-size: var(--font-size-body-sm);
    padding: var(--space-1) var(--space-2);
    border-radius: var(--radius-md);
    border: none;
    cursor: pointer;
    transition: var(--transition-tactile);

    &:focus-visible {
      outline: 2px solid var(--color-accent);
      outline-offset: 2px;
    }
  }
}

/* ═══ REDUCED MOTION ═══ */
@media (prefers-reduced-motion: reduce) {
  .ren-hover-card {
    &:popover-open {
      animation: none;
      opacity: 1;
      translate: 0 0;
    }

    &::before {
      display: none;
    }
  }
}

/* ═══ DARK MODE SUPPORT (OPTIONAL) ═══ */
@media (prefers-color-scheme: dark) {
  .ren-hover-card {
    /* ═══ USES CSS VARIABLES - AUTO ADAPTS ═══ */
  }
}

/* ═══ MOBILE RESPONSIVENESS ═══
   Hover cards keep a media query here because they're
   positioned absolutely relative to the viewport, and
   the constraint is viewport space, not container width. */
@media (max-width: 640px) {
  .ren-hover-card {
    max-width: calc(100vw - var(--space-4));
    min-width: auto;
    width: calc(100vw - var(--space-4));
  }
}

/* ═══ LOADING STATE ═══ */
.ren-hover-card.ren-hover-card-loading {
  pointer-events: none;

  & .ren-hover-card-body {
    opacity: 0.6;
  }
}

/* ═══ DISABLED STATE ═══ */
.ren-hover-card.ren-hover-card-disabled {
  display: none;
}

/* ═══ CUSTOM VARIANTS ═══ */
.ren-hover-card {
  &.ren-hover-card-sm {
    padding: var(--space-2);
    max-width: 15rem;
    min-width: 12rem;

    & .ren-hover-card-header {
      margin-bottom: var(--space-2);

      & h3,
      & h4 {
        font-size: var(--font-size-body-sm);
        margin-bottom: var(--space-0-25);
      }
    }

    & .ren-hover-card-body {
      margin-bottom: var(--space-2);
      font-size: var(--font-size-xs);
    }
  }

  &.ren-hover-card-lg {
    padding: var(--space-5);
    max-width: 25rem;
    min-width: 18rem;

    & .ren-hover-card-header {
      margin-bottom: var(--space-4);

      & h3,
      & h4 {
        font-size: var(--font-size-body);
      }
    }

    & .ren-hover-card-body {
      margin-bottom: var(--space-4);
      font-size: var(--font-size-body);
    }
  }
}

/* ═══ INNER LINKS STYLING ═══ */
.ren-hover-card a {
  color: var(--color-accent);
  text-decoration: underline;
  transition: var(--transition-tactile);

  &:hover {
    color: var(--color-accent-strong);
  }

  &:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 2px;
  }
}


id: file:components/composites/ren-hover-card/ren-hover-card.js
type: javascript
name: ren-hover-card.js
path: components/composites/ren-hover-card/ren-hover-card.js
data: {}
body:
/* ═══ REN HOVER CARD WEB COMPONENT ═══ */

export class RenHoverCard extends HTMLElement {
  constructor() {
    super();
    this.card = null;
    this.trigger = null;
    this.showDelay = 200;
    this.hideDelay = 300;
    this.showTimeout = null;
    this.hideTimeout = null;
    this.isOpen = false;

    /* ═══ BIND METHODS ═══ */
    this.handleTriggerMouseEnter = this.handleTriggerMouseEnter.bind(this);
    this.handleTriggerMouseLeave = this.handleTriggerMouseLeave.bind(this);
    this.handleTriggerFocusIn = this.handleTriggerFocusIn.bind(this);
    this.handleTriggerFocusOut = this.handleTriggerFocusOut.bind(this);
    this.handleCardMouseEnter = this.handleCardMouseEnter.bind(this);
    this.handleCardMouseLeave = this.handleCardMouseLeave.bind(this);
  }

  connectedCallback() {
    /* ═══ FIND OR CREATE CARD ═══ */
    this.card = this.querySelector('[role="tooltip"]') || this.querySelector('.ren-hover-card');

    if (!this.card) {
      console.warn('RenHoverCard: No card element found');
      return;
    }

    /* ═══ ENSURE CARD HAS CLASS ═══ */
    this.card.classList.add('ren-hover-card');

    /* ═══ SET UP POPOVER ATTRIBUTE ═══ */
    if (!this.card.hasAttribute('popover')) {
      this.card.setAttribute('popover', 'manual');
    }

    /* ═══ FIND TRIGGER ═══ */
    const triggerSelector = this.getAttribute('data-hover-trigger');
    if (triggerSelector) {
      this.trigger = document.querySelector(triggerSelector);
    } else {
      this.trigger = this.previousElementSibling;
    }

    if (!this.trigger) {
      console.warn('RenHoverCard: No trigger element found');
      return;
    }

    /* ═══ SET UP TRIGGER ARIA ATTRIBUTES ═══ */
    this.trigger.classList.add('ren-hover-card-trigger');
    this.trigger.setAttribute('aria-haspopup', 'tooltip');
    this.trigger.setAttribute('aria-expanded', 'false');

    /* ═══ READ DELAY ATTRIBUTES ═══ */
    const showDelayAttr = this.getAttribute('show-delay');
    const hideDelayAttr = this.getAttribute('hide-delay');
    if (showDelayAttr) this.showDelay = parseInt(showDelayAttr, 10);
    if (hideDelayAttr) this.hideDelay = parseInt(hideDelayAttr, 10);

    /* ═══ SET ANCHOR NAME ON TRIGGER ═══ */
    if (!this.trigger.style.anchorName) {
      this.trigger.style.anchorName = '--ren-hover-card-anchor';
    }

    /* ═══ SET POSITION ANCHOR ON CARD ═══ */
    if (!this.card.style.positionAnchor) {
      this.card.style.positionAnchor = '--ren-hover-card-anchor';
    }

    /* ═══ ATTACH EVENT LISTENERS ═══ */
    this.trigger.addEventListener('mouseenter', this.handleTriggerMouseEnter);
    this.trigger.addEventListener('mouseleave', this.handleTriggerMouseLeave);
    this.trigger.addEventListener('focusin', this.handleTriggerFocusIn);
    this.trigger.addEventListener('focusout', this.handleTriggerFocusOut);

    this.card.addEventListener('mouseenter', this.handleCardMouseEnter);
    this.card.addEventListener('mouseleave', this.handleCardMouseLeave);
  }

  disconnectedCallback() {
    if (this.trigger) {
      this.trigger.removeEventListener('mouseenter', this.handleTriggerMouseEnter);
      this.trigger.removeEventListener('mouseleave', this.handleTriggerMouseLeave);
      this.trigger.removeEventListener('focusin', this.handleTriggerFocusIn);
      this.trigger.removeEventListener('focusout', this.handleTriggerFocusOut);
    }

    if (this.card) {
      this.card.removeEventListener('mouseenter', this.handleCardMouseEnter);
      this.card.removeEventListener('mouseleave', this.handleCardMouseLeave);
    }

    this.clearTimeouts();
  }

  /* ═══ TRIGGER MOUSE ENTER ═══ */
  handleTriggerMouseEnter() {
    this.clearTimeouts();
    this.showTimeout = setTimeout(() => this.show(), this.showDelay);
  }

  /* ═══ TRIGGER MOUSE LEAVE ═══ */
  handleTriggerMouseLeave() {
    this.clearShowTimeout();
    /* ═══ GRACE PERIOD: KEEP OPEN IF MOUSE MOVES TO CARD ═══ */
    this.hideTimeout = setTimeout(() => this.hide(), this.hideDelay);
  }

  /* ═══ TRIGGER FOCUS IN (KEYBOARD) ═══ */
  handleTriggerFocusIn() {
    this.clearTimeouts();
    this.show();
  }

  /* ═══ TRIGGER FOCUS OUT (KEYBOARD) ═══ */
  handleTriggerFocusOut() {
    this.clearTimeouts();
    this.hideTimeout = setTimeout(() => this.hide(), this.hideDelay);
  }

  /* ═══ CARD MOUSE ENTER (GRACE AREA) ═══ */
  handleCardMouseEnter() {
    this.clearHideTimeout();
  }

  /* ═══ CARD MOUSE LEAVE ═══ */
  handleCardMouseLeave() {
    this.hideTimeout = setTimeout(() => this.hide(), this.hideDelay);
  }

  /* ═══ SHOW CARD ═══ */
  show() {
    if (!this.card) return;

    this.clearTimeouts();
    this.card.showPopover();
    this.trigger.setAttribute('aria-expanded', 'true');
    this.isOpen = true;

    this.dispatchEvent(
      new CustomEvent('ren-hover-card-open', {
        bubbles: true,
        composed: true,
      })
    );
  }

  /* ═══ HIDE CARD ═══ */
  hide() {
    if (!this.card) return;

    this.clearTimeouts();

    try {
      this.card.hidePopover();
    } catch {
      /* ═══ POPOVER API MAY NOT EXIST ═══ */
      this.card.style.display = 'none';
    }

    this.trigger.setAttribute('aria-expanded', 'false');
    this.isOpen = false;

    this.dispatchEvent(
      new CustomEvent('ren-hover-card-close', {
        bubbles: true,
        composed: true,
      })
    );
  }

  /* ═══ TOGGLE CARD ═══ */
  toggle() {
    if (this.isOpen) {
      this.hide();
    } else {
      this.show();
    }
  }

  /* ═══ CLEAR ALL TIMEOUTS ═══ */
  clearTimeouts() {
    this.clearShowTimeout();
    this.clearHideTimeout();
  }

  /* ═══ CLEAR SHOW TIMEOUT ═══ */
  clearShowTimeout() {
    if (this.showTimeout) {
      clearTimeout(this.showTimeout);
      this.showTimeout = null;
    }
  }

  /* ═══ CLEAR HIDE TIMEOUT ═══ */
  clearHideTimeout() {
    if (this.hideTimeout) {
      clearTimeout(this.hideTimeout);
      this.hideTimeout = null;
    }
  }

  /* ═══ GETTERS AND SETTERS ═══ */
  get open() {
    return this.isOpen;
  }

  set open(val) {
    if (val) {
      this.show();
    } else {
      this.hide();
    }
  }

  get disabled() {
    return this.hasAttribute('disabled');
  }

  set disabled(val) {
    if (val) {
      this.setAttribute('disabled', '');
      this.card?.classList.add('ren-hover-card-disabled');
    } else {
      this.removeAttribute('disabled');
      this.card?.classList.remove('ren-hover-card-disabled');
    }
  }
}

/* ═══ REGISTER COMPONENT ═══ */
if (!customElements.get('ren-hover-card')) {
  customElements.define('ren-hover-card', RenHoverCard);
}


id: file:components/composites/ren-menu/ren-menu.css
type: css
name: ren-menu.css
path: components/composites/ren-menu/ren-menu.css
data: {}
body:
/* ═══ MENU CONTAINER ═══ */
.ren-menu {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg, 0.5rem);
  box-shadow: var(--shadow-lg, 0 10px 25px rgb(0, 0, 0, 0.1));
  padding-block: var(--space-1, 0.25rem);
  min-width: 12rem;
  z-index: var(--z-dropdown, 1000);
  position: absolute;
  animation: ren-menu-open var(--duration-enter) var(--ease-enter);
  visibility: hidden;
  opacity: 0;
  inset: auto auto auto auto;
  margin: 0;
  max-height: calc(100vh - 32px);
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* ═══ MENU OPEN STATE ═══ */
.ren-menu[popover]:popover-open,
.ren-menu.ren-open {
  visibility: visible;
  opacity: 1;
}

/* ═══ MENU ITEM BASE ═══ */
.ren-menu-item {
  padding: var(--space-2, 0.5rem) var(--space-3, 0.75rem);
  display: flex;
  align-items: center;
  gap: var(--space-2, 0.5rem);
  cursor: pointer;
  font-size: var(--size-body, 1rem);
  color: var(--color-text);
  border-radius: var(--radius-sm, 0.25rem);
  min-height: 2rem;
  /* Tactile hover/focus — shared preset from semantic motion tokens */
  transition: var(--transition-tactile);
  white-space: nowrap;
  text-decoration: none;
}

.ren-menu-item:hover,
.ren-menu-item[data-highlighted] {
  background-color: var(--color-fill);
  color: var(--color-text);
}

.ren-menu-item:active {
  background-color: var(--color-fill-hover);
}

/* ═══ MENU ITEM DISABLED STATE ═══ */
.ren-menu-item[aria-disabled="true"] {
  opacity: 0.5;
  cursor: not-allowed;
  pointer-events: none;
}

/* ═══ MENU ITEM DANGER VARIANT ═══ */
.ren-menu-item.ren-menu-item-danger:hover,
.ren-menu-item.ren-menu-item-danger[data-highlighted] {
  background-color: var(--color-danger-subtle);
  color: var(--color-danger);
}

/* ═══ MENU ITEM ICON ═══ */
.ren-menu-item-icon {
  flex-shrink: 0;
  width: var(--size-icon-md, 1.25rem);
  height: var(--size-icon-md, 1.25rem);
  color: var(--color-text-muted);
  display: flex;
  align-items: center;
  justify-content: center;
}

/* ═══ MENU ITEM SHORTCUT/LABEL ═══ */
.ren-menu-item-shortcut {
  margin-inline-start: auto;
  font-size: var(--size-caption, 0.75rem);
  color: var(--color-text-muted);
  font-family: monospace;
  white-space: nowrap;
  flex-shrink: 0;
}

/* ═══ MENU ITEM DESCRIPTION ═══ */
.ren-menu-item-description {
  font-size: var(--size-caption, 0.75rem);
  color: var(--color-text-muted);
  line-height: 1.3;
}

/* ═══ MENU SEPARATOR ═══ */
.ren-menu-separator {
  height: 1px;
  background: var(--color-border);
  margin-block: var(--space-1, 0.25rem);
}

/* ═══ MENU LABEL/GROUP HEADER ═══ */
.ren-menu-label {
  padding: var(--space-1, 0.25rem) var(--space-3, 0.75rem);
  font-size: var(--size-caption, 0.75rem);
  font-weight: 600;
  color: var(--color-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

/* ═══ MENU GROUP ═══ */
.ren-menu-group {
  display: contents;
}

/* ═══ CHECKBOX MENU ITEM ═══ */
.ren-menu-checkbox-item,
.ren-menu-radio-item {
  position: relative;
  padding-inline-start: calc(var(--space-3, 0.75rem) + 20px + var(--space-2, 0.5rem));
}

.ren-menu-checkbox-item::before,
.ren-menu-radio-item::before {
  content: '';
  position: absolute;
  inset-inline-start: var(--space-3, 0.75rem);
  width: 16px;
  height: 16px;
  border: 2px solid var(--color-border);
  border-radius: 2px;
  background: white;
  /* Tactile — shared semantic preset */
  transition: var(--transition-tactile);
}

.ren-menu-radio-item::before {
  border-radius: 50%;
}

/* ═══ CHECKBOX/RADIO CHECKED STATE ═══ */
.ren-menu-checkbox-item[aria-checked="true"]::before,
.ren-menu-radio-item[aria-checked="true"]::before {
  background: var(--color-accent);
  border-color: var(--color-accent);
}

.ren-menu-checkbox-item[aria-checked="true"]::after,
.ren-menu-radio-item[aria-checked="true"]::after {
  content: '';
  position: absolute;
  inset-inline-start: calc(var(--space-3, 0.75rem) + 4px);
  top: 50%;
  transform: translateY(-50%);
}

.ren-menu-checkbox-item[aria-checked="true"]::after {
  width: 8px;
  height: 4px;
  border-inline-start: 2px solid white;
  border-bottom: 2px solid white;
  transform: translateY(-55%) rotate(-45deg);
}

.ren-menu-radio-item[aria-checked="true"]::after {
  width: 6px;
  height: 6px;
  background: white;
  border-radius: 50%;
  transform: translateY(-50%);
}

/* ═══ ANIMATIONS ═══ */
@keyframes ren-menu-open {
  from {
    opacity: 0;
    transform: translateY(-4px) scale(0.98);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

/* ═══ CLOSING STATE WITH ANIMATION ═══ */
.ren-menu[data-closing] {
  animation: ren-menu-close var(--duration-exit) var(--ease-exit) forwards;
}

@keyframes ren-menu-close {
  from {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
  to {
    opacity: 0;
    transform: translateY(-4px) scale(0.98);
  }
}

/* ═══ RESPECTS MOTION PREFERENCES ═══
   Semantic --duration-* already collapse to 0ms under reduced-motion
   (see tokens/semantic/motion.css). We keep this block as a
   belt-and-suspenders fallback and to drop the scale/translate
   keyframe so the menu simply appears/disappears in place. */
@media (prefers-reduced-motion: reduce) {
  .ren-menu {
    animation: none;
    transition: opacity var(--duration-enter);
  }

  .ren-menu:not(.ren-open) {
    opacity: 0;
  }

  .ren-menu-item {
    transition: background-color 0s, color 0s;
  }

  .ren-menu[data-closing] {
    animation: none;
  }
}

/* ═══ NATIVE POPOVER API SUPPORT ═══ */
@supports (selector(:popover-open)) {
  .ren-menu[popover] {
    visibility: visible;
    opacity: 0;
    transition: opacity var(--duration-enter) var(--ease-enter),
                overlay var(--duration-enter) var(--ease-enter) allow-discrete,
                display var(--duration-enter) var(--ease-enter) allow-discrete;
    animation: none;
  }

  @starting-style {
    .ren-menu[popover]:popover-open {
      opacity: 0;
    }
  }

  .ren-menu[popover]:popover-open {
    opacity: 1;
  }
}

/* ═══ SCROLLBAR STYLING ═══ */
.ren-menu::-webkit-scrollbar {
  width: 6px;
}

.ren-menu::-webkit-scrollbar-track {
  background: transparent;
}

.ren-menu::-webkit-scrollbar-thumb {
  background: var(--color-border);
  border-radius: 3px;
}

.ren-menu::-webkit-scrollbar-thumb:hover {
  background: var(--color-text-muted);
}
/* Appearance API bridge */
.ren-menu {
  min-inline-size: var(--ren-menu-width, 200px);
  padding: var(--ren-menu-padding, var(--space-1));
  border-radius: var(--ren-menu-radius, var(--radius-lg));
  background: var(--ren-menu-bg, var(--color-surface-overlay));
  border-color: var(--ren-menu-border, var(--color-border));
  box-shadow: var(--ren-menu-shadow, var(--shadow-lg));
  transition-duration: var(--ren-menu-duration, var(--duration-fast));
  transition-timing-function: var(--ren-menu-easing, var(--ease-out));
}
.ren-menu-item { min-block-size: var(--ren-menu-item-height, var(--size-md)); padding: var(--ren-menu-item-padding, var(--space-2) var(--space-3)); border-radius: var(--ren-menu-item-radius, var(--radius-sm)); }
.ren-menu-item:hover, .ren-menu-item[data-highlighted] { background: var(--ren-menu-item-hover-bg, var(--color-fill)); }
.ren-menu-item-danger { color: var(--ren-menu-danger-color, var(--color-danger)); }


id: file:components/composites/ren-menu/ren-menu.js
type: javascript
name: ren-menu.js
path: components/composites/ren-menu/ren-menu.js
data: {}
body:
/**
 * RenDS — <ren-menu> Web Component
 * ==================================
 * Dropdown menu component with keyboard navigation, accessibility, and theming.
 * Supports items, groups, separators, checkboxes, and radio items.
 *
 * Uses Light DOM — no Shadow DOM.
 * Native Popover API when available, falls back to class-based positioning.
 *
 * Attributes:
 *   placement:     'bottom-start' (default) | 'bottom-end' | 'top-start' | 'top-end'
 *                  | 'right-start' | 'right-end' | 'left-start' | 'left-end'
 *                  - Controls menu position relative to trigger
 *
 * Markup:
 *   <button data-menu-trigger id="menu-trigger">Menu</button>
 *   <ren-menu trigger-id="menu-trigger" placement="bottom-start">
 *     <button class="ren-menu-item" role="menuitem">Option 1</button>
 *     <button class="ren-menu-item" role="menuitem" data-value="opt2">Option 2</button>
 *     <div class="ren-menu-separator"></div>
 *     <button class="ren-menu-item ren-menu-item-danger" role="menuitem">Delete</button>
 *   </ren-menu>
 *
 * Features:
 *   - Arrow key navigation (vertical)
 *   - Home/End to first/last item
 *   - Enter/Space to select
 *   - Typeahead search
 *   - Click outside to close
 *   - Escape key to close
 *   - Focus management
 *   - Checkbox and radio item support
 *
 * Events:
 *   ren-menu-select:  Dispatched when an item is selected
 *   - detail: { item, value }
 *   ren-menu-open:    Dispatched when menu opens
 *   ren-menu-close:   Dispatched when menu closes
 */

import { createKeyboardNav } from '../../../utils/keyboard-nav.js';
import { createDismissable } from '../../../utils/dismissable.js';

const MENU_SIDES = new Set(['top', 'right', 'bottom', 'left']);
const MENU_ALIGNS = new Set(['start', 'end']);

function normalizeMenuPlacement(value) {
  const [sideValue, alignValue] = String(value || 'bottom-start')
    .toLowerCase()
    .split('-');
  const side = MENU_SIDES.has(sideValue) ? sideValue : 'bottom';
  const align = MENU_ALIGNS.has(alignValue) ? alignValue : 'start';

  return { side, align };
}

export class RenMenu extends HTMLElement {
  static observedAttributes = ['placement'];

  #trigger = null;
  #isOpen = false;
  #keyboardNav = null;
  #dismissable = null;
  #animationFrame = null;
  #boundItemClick = null;
  #boundTriggerClick = null;
  #boundTriggerKeydown = null;
  #returnFocus = true;
  #closeTimer = null;

  constructor() {
    super();
  }

  /**
   * Lifecycle: Element inserted into DOM
   */
  connectedCallback() {
    this.loadStyles();
    this.setupMenu();
    this.findTrigger();
    this.attachTriggerListeners();
    this.attachItemListeners();
  }

  /**
   * Lifecycle: Element removed from DOM
   */
  disconnectedCallback() {
    this.cleanup();
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'placement' && oldValue !== newValue) {
      this.syncPlacement();
      if (this.#isOpen) {
        this.positionMenu();
      }
    }
  }

  /* ═══ INITIALIZATION ═══ */

  /**
   * Load component styles
   * @private
   */
  loadStyles() {
    if (!document.getElementById('ren-menu-styles')) {
      const style = document.createElement('style');
      style.id = 'ren-menu-styles';
      style.textContent = this.constructor.styles || '';
      document.head.appendChild(style);
    }
  }

  /**
   * Setup menu element with required attributes and structure
   * @private
   */
  setupMenu() {
    this.classList.add('ren-menu');

    // Set accessibility attributes
    this.setAttribute('role', 'menu');
    this.id = this.id || `ren-menu-${Math.random().toString(36).substr(2, 9)}`;

    // Check for native Popover API support
    if ('popover' in HTMLElement.prototype) {
      this.setAttribute('popover', 'manual');
    }

    this.syncPlacement();

    // Ensure all items have correct roles if not already set
    this.querySelectorAll(
      '.ren-menu-item, .ren-menu-checkbox-item, .ren-menu-radio-item, [role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"]'
    ).forEach((item) => {
      if (!item.hasAttribute('role')) {
        if (item.classList.contains('ren-menu-checkbox-item')) {
          item.setAttribute('role', 'menuitemcheckbox');
        } else if (item.classList.contains('ren-menu-radio-item')) {
          item.setAttribute('role', 'menuitemradio');
        } else {
          item.setAttribute('role', 'menuitem');
        }
      }

      if (!item.hasAttribute('tabindex')) {
        item.setAttribute('tabindex', '-1');
      }
    });
  }

  /**
   * Find the trigger element
   * Searches for: trigger-id attribute, data-menu-trigger, or previous sibling
   * @private
   */
  findTrigger() {
    const triggerId = this.getAttribute('trigger-id');
    if (triggerId) {
      this.#trigger = document.getElementById(triggerId);
    }

    if (!this.#trigger) {
      this.#trigger = document.querySelector('[data-menu-trigger]');
    }

    if (!this.#trigger) {
      this.#trigger = this.previousElementSibling;
    }

    if (this.#trigger) {
      this.#trigger.setAttribute('aria-haspopup', 'menu');
      this.#trigger.setAttribute('aria-controls', this.id);
    }
  }

  /**
   * Attach event listeners to trigger element
   * @private
   */
  attachTriggerListeners() {
    if (!this.#trigger) return;
    this.#boundTriggerClick = (e) => this.handleTriggerClick(e);
    this.#boundTriggerKeydown = (e) => this.handleTriggerKeydown(e);
    this.#trigger.addEventListener('click', this.#boundTriggerClick);
    this.#trigger.addEventListener('keydown', this.#boundTriggerKeydown);
  }

  /**
   * Attach delegated item listeners
   * @private
   */
  attachItemListeners() {
    this.#boundItemClick = this.#boundItemClick || ((e) => this.handleItemClick(e));
    this.removeEventListener('click', this.#boundItemClick);
    this.addEventListener('click', this.#boundItemClick);
  }

  /* ═══ TRIGGER HANDLING ═══ */

  /**
   * Handle trigger click to open menu
   * @private
   */
  handleTriggerClick(e) {
    e.preventDefault();
    if (this.#isOpen) {
      this.close();
    } else {
      this.open();
    }
  }

  /**
   * Handle trigger keydown (arrow down to open)
   * @private
   */
  handleTriggerKeydown(e) {
    if ((e.key === 'ArrowDown' || e.key === 'ArrowUp') && !this.#isOpen) {
      e.preventDefault();
      this.open();
    }
  }

  /**
   * Handle item click selection
   * @private
   */
  handleItemClick(e) {
    const item = e.target.closest('[role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"]');

    if (!item || !this.contains(item) || item.getAttribute('aria-disabled') === 'true') {
      return;
    }

    e.preventDefault();
    e.stopPropagation();
    this.handleItemSelect(item);
  }

  /* ═══ POSITIONING ═══ */

  /**
   * Mirror preferred placement to public data attributes.
   */
  syncPlacement() {
    const { side, align } = normalizeMenuPlacement(this.getAttribute('placement'));

    this.setAttribute('data-side', side);
    this.setAttribute('data-align', align);
  }

  /**
   * Position the menu relative to trigger element
   * @private
   */
  positionMenu() {
    if (!this.#trigger) return;

    const placement = normalizeMenuPlacement(this.getAttribute('placement'));
    const triggerRect = this.#trigger.getBoundingClientRect();
    const menuRect = this.getBoundingClientRect();
    const gap = 8;
    const margin = 8;

    let finalSide = placement.side;
    let left = triggerRect.left;
    let top = triggerRect.bottom + gap;

    if (placement.align === 'end') {
      left = triggerRect.right - menuRect.width;
    }

    if (placement.side === 'top') {
      top = triggerRect.top - menuRect.height - gap;
    } else if (placement.side === 'right') {
      left = triggerRect.right + gap;
      top = placement.align === 'end'
        ? triggerRect.bottom - menuRect.height
        : triggerRect.top;
    } else if (placement.side === 'left') {
      left = triggerRect.left - menuRect.width - gap;
      top = placement.align === 'end'
        ? triggerRect.bottom - menuRect.height
        : triggerRect.top;
    }

    // Viewport collision detection
    const viewport = {
      width: window.innerWidth,
      height: window.innerHeight,
    };

    if (
      placement.side === 'bottom' &&
      top + menuRect.height > viewport.height - margin &&
      triggerRect.top - menuRect.height - gap >= margin
    ) {
      finalSide = 'top';
      top = triggerRect.top - menuRect.height - gap;
    } else if (
      placement.side === 'top' &&
      top < margin &&
      triggerRect.bottom + menuRect.height + gap <= viewport.height - margin
    ) {
      finalSide = 'bottom';
      top = triggerRect.bottom + gap;
    } else if (
      placement.side === 'right' &&
      left + menuRect.width > viewport.width - margin &&
      triggerRect.left - menuRect.width - gap >= margin
    ) {
      finalSide = 'left';
      left = triggerRect.left - menuRect.width - gap;
    } else if (
      placement.side === 'left' &&
      left < margin &&
      triggerRect.right + menuRect.width + gap <= viewport.width - margin
    ) {
      finalSide = 'right';
      left = triggerRect.right + gap;
    }

    // Clamp horizontal and vertical position to viewport.
    if (left < margin) {
      left = margin;
    } else if (left + menuRect.width > viewport.width - margin) {
      left = viewport.width - menuRect.width - margin;
    }

    if (top < margin) {
      top = margin;
    } else if (top + menuRect.height > viewport.height - margin) {
      top = viewport.height - menuRect.height - margin;
    }

    this.setAttribute('data-side', finalSide);
    this.setAttribute('data-align', placement.align);
    this.style.left = `${left}px`;
    this.style.top = `${top}px`;
  }

  /* ═══ KEYBOARD NAVIGATION ═══ */

  /**
   * Setup keyboard navigation for menu items
   * @private
   */
  setupKeyboardNav() {
    if (this.#keyboardNav) {
      this.#keyboardNav.detach();
    }

    this.#keyboardNav = createKeyboardNav(this, {
      selector: '[role="menuitem"]:not([aria-disabled="true"]), [role="menuitemcheckbox"]:not([aria-disabled="true"]), [role="menuitemradio"]:not([aria-disabled="true"])',
      orientation: 'vertical',
      loop: true,
      typeahead: true,
      focusOnHover: true,
      onActivate: (item) => {
        item.focus();
      },
      onSelect: (item) => {
        this.handleItemSelect(item);
      },
    });

    this.#keyboardNav.attach();
  }

  /**
   * Teardown keyboard navigation
   * @private
   */
  teardownKeyboardNav() {
    if (this.#keyboardNav) {
      this.#keyboardNav.detach();
      this.#keyboardNav = null;
    }
  }

  /* ═══ DISMISSABLE LAYER ═══ */

  /**
   * Setup dismissable layer (click-outside, Escape key)
   * @private
   */
  setupDismissable() {
    if (this.#dismissable) {
      this.#dismissable.deactivate();
    }

    this.#dismissable = createDismissable(this, {
      onDismiss: () => this.close(),
      triggerElement: this.#trigger,
      escapeKey: true,
      clickOutside: true,
    });

    this.#dismissable.activate();
  }

  /**
   * Teardown dismissable layer
   * @private
   */
  teardownDismissable() {
    if (this.#dismissable) {
      this.#dismissable.deactivate();
      this.#dismissable = null;
    }
  }

  /* ═══ ITEM SELECTION ═══ */

  /**
   * Handle menu item selection
   * @private
   */
  handleItemSelect(item) {
    const role = item.getAttribute('role');

    // Handle checkbox items
    if (item.classList.contains('ren-menu-checkbox-item') || role === 'menuitemcheckbox') {
      const isChecked = item.getAttribute('aria-checked') === 'true';
      item.setAttribute('aria-checked', !isChecked ? 'true' : 'false');
      this.dispatchSelectEvent(item);
      return;
    }

    // Handle radio items (exclusive selection within group)
    if (item.classList.contains('ren-menu-radio-item') || role === 'menuitemradio') {
      const group = item.closest('.ren-menu-radio-group') || this;
      group
        .querySelectorAll('.ren-menu-radio-item, [role="menuitemradio"]')
        .forEach((radioItem) => radioItem.setAttribute('aria-checked', 'false'));
      item.setAttribute('aria-checked', 'true');
      this.dispatchSelectEvent(item);
      this.close();
      return;
    }

    // Regular menu item: dispatch event and close
    this.dispatchSelectEvent(item);
    this.close();
  }

  /**
   * Dispatch ren-menu-select event
   * @private
   */
  dispatchSelectEvent(item) {
    const value = item.getAttribute('data-value') || item.textContent.trim();
    this.dispatchEvent(
      new CustomEvent('ren-menu-select', {
        bubbles: true,
        detail: { item, value },
      })
    );
  }

  /* ═══ PUBLIC API ═══ */

  /**
   * Open the menu
   */
  open() {
    if (this.#isOpen) return;

    this.#isOpen = true;
    this.#returnFocus = true;
    this.setAttribute('data-state', 'open');
    this.setupKeyboardNav();
    this.setupDismissable();

    this.positionMenu();

    if ('popover' in HTMLElement.prototype) {
      try {
        this.showPopover();
      } catch {
        // Popover might already be open
      }
    } else {
      this.classList.add('ren-open');
    }

    // Reposition on next frame for accuracy
    this.#animationFrame = requestAnimationFrame(() => {
      this.positionMenu();

      // Focus first item
      const firstItem = this.querySelector('[role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"]');
      if (firstItem) {
        firstItem.focus();
      }
    });

    this.#trigger?.setAttribute('aria-expanded', 'true');
    this.dispatchEvent(new CustomEvent('ren-menu-open', { bubbles: true }));
  }

  /**
   * Close the menu
   */
  close() {
    if (!this.#isOpen) return;

    this.#isOpen = false;
    this.setAttribute('data-state', 'closed');
    this.teardownKeyboardNav();
    this.teardownDismissable();

    // Add closing animation class
    this.setAttribute('data-closing', '');

    // Wait for animation to finish before actually closing
    const animationDuration = getComputedStyle(this).animationDuration;
    const duration = parseFloat(animationDuration) * 1000;

    if (this.#closeTimer) clearTimeout(this.#closeTimer);
    this.#closeTimer = setTimeout(() => {
      this.removeAttribute('data-closing');

      if ('popover' in HTMLElement.prototype) {
        try {
          this.hidePopover();
        } catch {
          // Popover might already be closed
        }
      } else {
        this.classList.remove('ren-open');
      }

      this.#trigger?.setAttribute('aria-expanded', 'false');
      if (this.#returnFocus && this.#trigger && document.contains(this.#trigger)) {
        this.#trigger.focus();
      }
      this.#closeTimer = null;
    }, Math.min(duration, 150)); // Cap at 150ms

    this.dispatchEvent(new CustomEvent('ren-menu-close', { bubbles: true }));
  }

  /**
   * Check if menu is currently open
   * @returns {boolean}
   */
  isOpen() {
    return this.#isOpen;
  }

  /**
   * Get the trigger element
   * @returns {HTMLElement|null}
   */
  getTrigger() {
    return this.#trigger;
  }

  /**
   * Cleanup event listeners and resources
   * @private
   */
  cleanup() {
    this.close();
    if (this.#closeTimer) clearTimeout(this.#closeTimer);
    this.teardownKeyboardNav();
    this.teardownDismissable();

    if (this.#animationFrame) {
      cancelAnimationFrame(this.#animationFrame);
    }

    if (this.#boundItemClick) {
      this.removeEventListener('click', this.#boundItemClick);
    }

    if (this.#trigger) {
      this.#trigger.removeEventListener('click', this.#boundTriggerClick);
      this.#trigger.removeEventListener('keydown', this.#boundTriggerKeydown);
    }
  }
}

if (!customElements.get('ren-menu')) {
  customElements.define('ren-menu', RenMenu);
}

// Compatibility export: context-menu behavior and registration are owned by
// the colocated context-menu module so importing either historical path cannot
// install a second implementation.
export { RenContextMenu } from '../ren-context-menu/ren-context-menu.js';


id: file:components/composites/ren-number-field/ren-number-field.css
type: css
name: ren-number-field.css
path: components/composites/ren-number-field/ren-number-field.css
data: {}
body:
/* ═══════════════════════════════════════════════════════════════
   REN NUMBER FIELD (STEPPER) COMPONENT
   ═══════════════════════════════════════════════════════════════
   A controlled number input with increment/decrement buttons.
   Supports keyboard navigation, long-press acceleration, and
   min/max bounds. Follows RenDS design token system.

   Usage:
   <ren-number-field min="0" max="100" step="1" value="5">
     <button class="ren-number-field-decrement" aria-label="Decrease">−</button>
     <input class="ren-number-field-input" type="number" value="5">
     <button class="ren-number-field-increment" aria-label="Increase">+</button>
   </ren-number-field>
   ═══════════════════════════════════════════════════════════════ */

/* ═══ BASE WRAPPER ═══ */
.ren-number-field {
  display: inline-flex;
  align-items: center;
  gap: 0;
  border: var(--stroke-1) solid var(--color-input-border);
  border-radius: var(--radius-md);
  background-color: var(--color-input-bg);
  transition: var(--transition-tactile);

  /* ═══ FOCUS STATE ON GROUP ═══ */
  &:has(.ren-number-field-input:focus) {
    border-color: var(--color-input-border-focus);
    box-shadow: 0 0 0 3px var(--color-input-focus-ring);
    background-color: var(--color-input-bg-hover);
  }

  /* ═══ HOVER STATE ═══ */
  &:hover:not(:has(:disabled)) {
    background-color: var(--color-input-bg-hover);
  }

  /* ═══ DISABLED STATE ═══ */
  &:has(:disabled) {
    background-color: var(--color-disabled-bg);
    border-color: var(--color-border);
    cursor: not-allowed;
    opacity: 0.7;
  }
}

/* ═══ STEPPER BUTTONS ═══ */
.ren-number-field-decrement,
.ren-number-field-increment {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: var(--touch-min);
  height: var(--touch-min);
  padding: 0 var(--space-2);
  background: transparent;
  border: none;
  color: var(--color-text);
  cursor: pointer;
  font-size: var(--text-lg);
  font-weight: 600;
  font-family: var(--font-mono);
  transition: var(--transition-tactile);

  /* ═══ REMOVE FOCUS OUTLINE - GROUP HANDLES IT ═══ */
  &:focus {
    outline: none;
  }

  /* ═══ HOVER STATE ═══ */
  &:hover:not(:disabled) {
    background-color: var(--color-fill-hover);
  }

  /* ═══ ACTIVE STATE ═══ */
  &:active:not(:disabled) {
    background-color: var(--color-fill-active);
  }

  /* ═══ DISABLED STATE ═══ */
  &:disabled {
    color: var(--color-disabled-text);
    cursor: not-allowed;
    opacity: 0.5;
  }
}

/* ═══ DECREMENT BUTTON (LEFT) ═══ */
.ren-number-field-decrement {
  border-inline-end: var(--stroke-1) solid var(--color-border);
  border-radius: var(--radius-md) 0 0 var(--radius-md);

  /* ═══ MINUS ICON ═══ */
  &::before {
    content: '−';
  }
}

/* ═══ INCREMENT BUTTON (RIGHT) ═══ */
.ren-number-field-increment {
  border-inline-start: var(--stroke-1) solid var(--color-border);
  border-radius: 0 var(--radius-md) var(--radius-md) 0;

  /* ═══ PLUS ICON ═══ */
  &::before {
    content: '+';
  }
}

/* ═══ INPUT FIELD (CENTER) ═══ */
.ren-number-field-input {
  flex: 1;
  min-width: 3rem;
  padding: var(--space-2) var(--space-3);
  font-size: var(--body-size);
  font-family: var(--font-mono);
  font-weight: 500;
  color: var(--color-text);
  text-align: center;
  background: transparent;
  border: none;
  outline: none;
  cursor: text;

  /* ═══ HIDE NATIVE NUMBER SPINNER ═══ */
  &::-webkit-outer-spin-button,
  &::-webkit-inner-spin-button {
    -webkit-appearance: none;
    margin: 0;
  }

  &[type='number'] {
    -moz-appearance: textfield;
  }

  /* ═══ PLACEHOLDER ═══ */
  &::placeholder {
    color: var(--color-input-placeholder);
  }

  /* ═══ DISABLED ═══ */
  &:disabled {
    cursor: not-allowed;
    color: var(--color-disabled-text);
  }
}

/* ═══ VARIANT: SMALL ═══ */
.ren-number-field-sm {
  height: var(--size-sm);
  border-radius: var(--radius-sm);

  & .ren-number-field-decrement,
  & .ren-number-field-increment {
    min-width: var(--size-sm);
    height: var(--size-sm);
    padding: 0 var(--space-1);
    font-size: var(--text-sm);
  }

  & .ren-number-field-decrement {
    border-radius: var(--radius-sm) 0 0 var(--radius-sm);
  }

  & .ren-number-field-increment {
    border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  }

  & .ren-number-field-input {
    padding: var(--space-1) var(--space-2);
    font-size: var(--text-sm);
  }
}

/* ═══ VARIANT: LARGE ═══ */
.ren-number-field-lg {
  height: var(--size-lg);
  border-radius: var(--radius-lg);

  & .ren-number-field-decrement,
  & .ren-number-field-increment {
    min-width: var(--size-lg);
    height: var(--size-lg);
    padding: 0 var(--space-3);
    font-size: var(--text-lg);
  }

  & .ren-number-field-decrement {
    border-radius: var(--radius-lg) 0 0 var(--radius-lg);
  }

  & .ren-number-field-increment {
    border-radius: 0 var(--radius-lg) var(--radius-lg) 0;
  }

  & .ren-number-field-input {
    padding: var(--space-3) var(--space-4);
    font-size: var(--text-lg);
  }
}

/* ═══ ERROR STATE ═══ */
.ren-number-field[data-invalid] {
  border-color: var(--color-danger);

  &:focus-within {
    box-shadow: 0 0 0 3px rgb(255, 59, 48, 0.15);
  }
}

/* ═══ SUCCESS STATE ═══ */
.ren-number-field[data-valid] {
  border-color: var(--color-success);

  &:focus-within {
    box-shadow: 0 0 0 3px rgb(52, 199, 89, 0.15);
  }
}

/* ═══ REDUCED MOTION ═══ */
@media (prefers-reduced-motion: reduce) {
  .ren-number-field,
  .ren-number-field-decrement,
  .ren-number-field-increment {
    transition: none;
  }
}


id: file:components/composites/ren-number-field/ren-number-field.js
type: javascript
name: ren-number-field.js
path: components/composites/ren-number-field/ren-number-field.js
data: {}
body:
/* ═══════════════════════════════════════════════════════════════
   REN NUMBER FIELD (STEPPER) WEB COMPONENT
   ═══════════════════════════════════════════════════════════════
   A light-DOM web component that enhances a number input with
   increment/decrement buttons. Handles keyboard navigation,
   long-press auto-repeat with acceleration, and min/max bounds.

   Attributes:
   - min: Minimum value (default: 0)
   - max: Maximum value (default: 100)
   - step: Step size (default: 1)
   - value: Current value
   - disabled: Disable the field

   Events:
   - ren-change: Dispatched on value change { value, oldValue }

   Methods:
   - increment(): Increase value by step
   - decrement(): Decrease value by step
   - getValue(): Return current numeric value
   - setValue(val): Set value and clamp to bounds
   ═══════════════════════════════════════════════════════════════ */

export class RenNumberField extends HTMLElement {
  constructor() {
    super();

    /* ═══ STATE ═══ */
    this.min = 0;
    this.max = 100;
    this.step = 1;
    this.value = 0;
    this.pressTimeout = null;
    this.pressInterval = null;
    this.pressRepeatDelay = 500;
    this.pressRepeatInterval = 100;
    this.pressRepeatAcceleration = 0.95;

    /* ═══ BIND METHODS ═══ */
    this.handleDecrementPress = this.handleDecrementPress.bind(this);
    this.handleIncrementPress = this.handleIncrementPress.bind(this);
    this.handleDecrementRelease = this.handleDecrementRelease.bind(this);
    this.handleIncrementRelease = this.handleIncrementRelease.bind(this);
    this.handleInputChange = this.handleInputChange.bind(this);
    this.handleInputKeydown = this.handleInputKeydown.bind(this);
  }

  connectedCallback() {
    /* ═══ READ ATTRIBUTES ═══ */
    const minAttr = this.getAttribute('min');
    const maxAttr = this.getAttribute('max');
    const stepAttr = this.getAttribute('step');
    const valueAttr = this.getAttribute('value');
    const disabledAttr = this.hasAttribute('disabled');

    if (minAttr !== null) this.min = parseFloat(minAttr);
    if (maxAttr !== null) this.max = parseFloat(maxAttr);
    if (stepAttr !== null) this.step = parseFloat(stepAttr);
    if (valueAttr !== null) this.value = parseFloat(valueAttr);

    /* ═══ GET OR CREATE ELEMENTS ═══ */
    this.input = this.querySelector('.ren-number-field-input');
    this.decrementBtn = this.querySelector('.ren-number-field-decrement');
    this.incrementBtn = this.querySelector('.ren-number-field-increment');

    /* ═══ CREATE IF MISSING ═══ */
    if (!this.input) {
      this.input = document.createElement('input');
      this.input.type = 'number';
      this.input.className = 'ren-number-field-input';
      this.appendChild(this.input);
    }

    if (!this.decrementBtn) {
      this.decrementBtn = document.createElement('button');
      this.decrementBtn.className = 'ren-number-field-decrement';
      this.decrementBtn.setAttribute('aria-label', 'Decrease');
      this.insertBefore(this.decrementBtn, this.input);
    }

    if (!this.incrementBtn) {
      this.incrementBtn = document.createElement('button');
      this.incrementBtn.className = 'ren-number-field-increment';
      this.incrementBtn.setAttribute('aria-label', 'Increase');
      this.appendChild(this.incrementBtn);
    }

    // Steppers are controls for the number input, never form submitters.
    this.decrementBtn.type = 'button';
    this.incrementBtn.type = 'button';

    /* ═══ SET INITIAL VALUES ═══ */
    this.input.value = this.value.toString();
    this.input.min = this.min.toString();
    this.input.max = this.max.toString();
    this.input.step = this.step.toString();

    if (disabledAttr) {
      this.input.disabled = true;
      this.decrementBtn.disabled = true;
      this.incrementBtn.disabled = true;
    }

    /* ═══ ATTACH EVENT LISTENERS ═══ */
    this.input.addEventListener('change', this.handleInputChange);
    this.input.addEventListener('keydown', this.handleInputKeydown);

    /* ═══ BUTTON PRESS & HOLD ═══ */
    this.decrementBtn.addEventListener('mousedown', this.handleDecrementPress);
    this.decrementBtn.addEventListener('mouseup', this.handleDecrementRelease);
    this.decrementBtn.addEventListener('mouseleave', this.handleDecrementRelease);
    this.decrementBtn.addEventListener('touchstart', this.handleDecrementPress);
    this.decrementBtn.addEventListener('touchend', this.handleDecrementRelease);

    this.incrementBtn.addEventListener('mousedown', this.handleIncrementPress);
    this.incrementBtn.addEventListener('mouseup', this.handleIncrementRelease);
    this.incrementBtn.addEventListener('mouseleave', this.handleIncrementRelease);
    this.incrementBtn.addEventListener('touchstart', this.handleIncrementPress);
    this.incrementBtn.addEventListener('touchend', this.handleIncrementRelease);
  }

  disconnectedCallback() {
    /* ═══ CLEAN UP EVENT LISTENERS ═══ */
    if (this.input) {
      this.input.removeEventListener('change', this.handleInputChange);
      this.input.removeEventListener('keydown', this.handleInputKeydown);
    }

    if (this.decrementBtn) {
      this.decrementBtn.removeEventListener('mousedown', this.handleDecrementPress);
      this.decrementBtn.removeEventListener('mouseup', this.handleDecrementRelease);
      this.decrementBtn.removeEventListener('mouseleave', this.handleDecrementRelease);
      this.decrementBtn.removeEventListener('touchstart', this.handleDecrementPress);
      this.decrementBtn.removeEventListener('touchend', this.handleDecrementRelease);
    }

    if (this.incrementBtn) {
      this.incrementBtn.removeEventListener('mousedown', this.handleIncrementPress);
      this.incrementBtn.removeEventListener('mouseup', this.handleIncrementRelease);
      this.incrementBtn.removeEventListener('mouseleave', this.handleIncrementRelease);
      this.incrementBtn.removeEventListener('touchstart', this.handleIncrementPress);
      this.incrementBtn.removeEventListener('touchend', this.handleIncrementRelease);
    }

    /* ═══ CLEAR TIMERS ═══ */
    this.clearPressTimers();
  }

  /* ═══════════════════════════════════════════════════════════════
     DECREMENT BUTTON HANDLERS
     ═══════════════════════════════════════════════════════════════ */
  handleDecrementPress = (e) => {
    if (this.input.disabled || this.decrementBtn.disabled) return;
    e.preventDefault();

    this.decrement();

    /* ═══ START LONG PRESS REPEAT ═══ */
    this.pressTimeout = setTimeout(() => {
      let repeatInterval = this.pressRepeatInterval;

      this.pressInterval = setInterval(() => {
        this.decrement();
        /* ═══ ACCELERATE ═══ */
        repeatInterval *= this.pressRepeatAcceleration;
        clearInterval(this.pressInterval);

        this.pressInterval = setInterval(
          () => this.decrement(),
          Math.max(50, repeatInterval)
        );
      }, this.pressRepeatInterval);
    }, this.pressRepeatDelay);
  };

  handleDecrementRelease = () => {
    this.clearPressTimers();
  };

  /* ═══════════════════════════════════════════════════════════════
     INCREMENT BUTTON HANDLERS
     ═══════════════════════════════════════════════════════════════ */
  handleIncrementPress = (e) => {
    if (this.input.disabled || this.incrementBtn.disabled) return;
    e.preventDefault();

    this.increment();

    /* ═══ START LONG PRESS REPEAT ═══ */
    this.pressTimeout = setTimeout(() => {
      let repeatInterval = this.pressRepeatInterval;

      this.pressInterval = setInterval(() => {
        this.increment();
        /* ═══ ACCELERATE ═══ */
        repeatInterval *= this.pressRepeatAcceleration;
        clearInterval(this.pressInterval);

        this.pressInterval = setInterval(
          () => this.increment(),
          Math.max(50, repeatInterval)
        );
      }, this.pressRepeatInterval);
    }, this.pressRepeatDelay);
  };

  handleIncrementRelease = () => {
    this.clearPressTimers();
  };

  /* ═══════════════════════════════════════════════════════════════
     INPUT HANDLERS
     ═══════════════════════════════════════════════════════════════ */
  handleInputChange = (e) => {
    const newValue = parseFloat(e.target.value);

    if (!isNaN(newValue)) {
      const oldValue = this.value;
      this.value = this.clamp(newValue);
      this.input.value = this.value.toString();

      this.dispatchChangeEvent(oldValue);
    }
  };

  handleInputKeydown = (e) => {
    if (e.key === 'ArrowUp') {
      e.preventDefault();
      this.increment();
    } else if (e.key === 'ArrowDown') {
      e.preventDefault();
      this.decrement();
    } else if (e.key === 'Home') {
      e.preventDefault();
      const oldValue = this.value;
      this.value = this.min;
      this.input.value = this.value.toString();
      this.dispatchChangeEvent(oldValue);
    } else if (e.key === 'End') {
      e.preventDefault();
      const oldValue = this.value;
      this.value = this.max;
      this.input.value = this.value.toString();
      this.dispatchChangeEvent(oldValue);
    }
  };

  /* ═══════════════════════════════════════════════════════════════
     PUBLIC METHODS
     ═══════════════════════════════════════════════════════════════ */
  increment() {
    const oldValue = this.value;
    this.value = this.clamp(this.value + this.step);
    this.input.value = this.value.toString();
    this.dispatchChangeEvent(oldValue);
  }

  decrement() {
    const oldValue = this.value;
    this.value = this.clamp(this.value - this.step);
    this.input.value = this.value.toString();
    this.dispatchChangeEvent(oldValue);
  }

  getValue() {
    return this.value;
  }

  setValue(val) {
    const oldValue = this.value;
    this.value = this.clamp(parseFloat(val) || 0);
    this.input.value = this.value.toString();
    this.dispatchChangeEvent(oldValue);
  }

  /* ═══════════════════════════════════════════════════════════════
     UTILITY METHODS
     ═══════════════════════════════════════════════════════════════ */
  clamp(val) {
    return Math.max(this.min, Math.min(this.max, val));
  }

  dispatchChangeEvent(oldValue) {
    if (this.value !== oldValue) {
      this.dispatchEvent(
        new CustomEvent('ren-change', {
          bubbles: true,
          composed: true,
          detail: {
            value: this.value,
            oldValue: oldValue,
          },
        })
      );
    }
  }

  clearPressTimers() {
    if (this.pressTimeout) {
      clearTimeout(this.pressTimeout);
      this.pressTimeout = null;
    }

    if (this.pressInterval) {
      clearInterval(this.pressInterval);
      this.pressInterval = null;
    }
  }

  /* ═══════════════════════════════════════════════════════════════
     PROPERTIES
     ═══════════════════════════════════════════════════════════════ */
  get disabled() {
    return this.hasAttribute('disabled');
  }

  set disabled(val) {
    if (val) {
      this.setAttribute('disabled', '');
      if (this.input) this.input.disabled = true;
      if (this.decrementBtn) this.decrementBtn.disabled = true;
      if (this.incrementBtn) this.incrementBtn.disabled = true;
    } else {
      this.removeAttribute('disabled');
      if (this.input) this.input.disabled = false;
      if (this.decrementBtn) this.decrementBtn.disabled = false;
      if (this.incrementBtn) this.incrementBtn.disabled = false;
    }
  }
}

/* ═══ REGISTER COMPONENT ═══ */
if (!customElements.get('ren-number-field')) {
  customElements.define('ren-number-field', RenNumberField);
}


id: file:components/composites/ren-otp/ren-otp.css
type: css
name: ren-otp.css
path: components/composites/ren-otp/ren-otp.css
data: {}
body:
/* ═══════════════════════════════════════════════════════════════
   REN OTP (ONE-TIME PASSWORD) INPUT COMPONENT
   ═══════════════════════════════════════════════════════════════
   A flexible OTP input with auto-advancing slots, paste support,
   and validation states. Handles numeric and alphanumeric codes.
   Follows RenDS design token system.

   Usage:
   <ren-otp length="6" type="numeric">
     \3c !-- Slots auto-generated by JS -->
   </ren-otp>

   Attributes:
   - length: Number of OTP digits (default: 6)
   - type: "numeric" or "alphanumeric" (default: "numeric")
   - disabled: Disable all slots

   Data Attributes:
   - data-valid: Success state
   - data-invalid: Error state
   - data-filled: On individual slots when filled
   ═══════════════════════════════════════════════════════════════ */

/* ═══ BASE WRAPPER ═══ */
.ren-otp {
  container-type: inline-size;
  container-name: ren-otp;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
}

/* ═══ INDIVIDUAL SLOT ═══ */
.ren-otp-slot {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--touch-min);
  height: var(--touch-min);
  padding: 0;
  font-size: var(--text-lg);
  font-family: var(--font-mono);
  font-weight: 600;
  color: var(--color-text);
  background-color: var(--color-input-bg);
  border: var(--stroke-1) solid var(--color-input-border);
  border-radius: var(--radius-md);
  outline: none;
  cursor: text;
  caret-color: var(--color-accent);
  text-align: center;
  transition: var(--transition-tactile);

  /* ═══ PLACEHOLDER STYLING ═══ */
  &::placeholder {
    color: var(--color-input-placeholder);
  }

  /* ═══ HOVER STATE ═══ */
  &:hover:not(:disabled):not(:focus) {
    background-color: var(--color-input-bg-hover);
  }

  /* ═══ FOCUS STATE ═══ */
  &:focus {
    border-color: var(--color-input-border-focus);
    box-shadow: 0 0 0 3px var(--color-input-focus-ring);
    background-color: var(--color-input-bg-hover);
  }

  /* ═══ FILLED STATE ═══ */
  &[data-filled] {
    background-color: var(--color-fill);
    border-color: var(--color-input-border);
  }

  /* ═══ DISABLED STATE ═══ */
  &:disabled {
    background-color: var(--color-disabled-bg);
    color: var(--color-disabled-text);
    cursor: not-allowed;
    opacity: 0.7;
    border-color: var(--color-border);
  }
}

/* ═══ SEPARATOR ═══ */
.ren-otp-separator {
  display: flex;
  align-items: center;
  justify-content: center;
  width: auto;
  height: var(--touch-min);
  padding: 0 var(--space-1);
  color: var(--color-text-muted);
  font-size: var(--text-lg);
  font-weight: 300;
  user-select: none;
  pointer-events: none;
}

/* ═══════════════════════════════════════════════════════════════
   VALIDATION STATES
   ═══════════════════════════════════════════════════════════════ */

/* ═══ ERROR STATE ═══ */
.ren-otp[data-invalid] .ren-otp-slot {
  border-color: var(--color-danger);

  &:focus {
    box-shadow: 0 0 0 3px rgb(255, 59, 48, 0.15);
    border-color: var(--color-danger);
  }
}

/* ═══ SUCCESS STATE ═══ */
.ren-otp[data-valid] .ren-otp-slot {
  border-color: var(--color-success);

  &:focus {
    box-shadow: 0 0 0 3px rgb(52, 199, 89, 0.15);
    border-color: var(--color-success);
  }
}

/* ═══════════════════════════════════════════════════════════════
   SIZE VARIANTS
   ═══════════════════════════════════════════════════════════════ */

/* ═══ SMALL VARIANT ═══ */
.ren-otp-sm {
  gap: var(--space-1);

  & .ren-otp-slot {
    width: var(--size-sm);
    height: var(--size-sm);
    font-size: var(--text-sm);
    border-radius: var(--radius-sm);
  }

  & .ren-otp-separator {
    height: var(--size-sm);
    font-size: var(--text-sm);
  }
}

/* ═══ LARGE VARIANT ═══ */
.ren-otp-lg {
  gap: var(--space-3);

  & .ren-otp-slot {
    width: var(--size-lg);
    height: var(--size-lg);
    font-size: var(--body-size);
    border-radius: var(--radius-lg);
  }

  & .ren-otp-separator {
    height: var(--size-lg);
    font-size: var(--body-size);
  }
}

/* ═══════════════════════════════════════════════════════════════
   ACCESSIBILITY
   ═══════════════════════════════════════════════════════════════ */

/* ═══ FOCUS VISIBLE (KEYBOARD NAVIGATION) ═══ */
.ren-otp-slot:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* ═══ DISABLED WRAPPER ═══ */
.ren-otp[disabled] .ren-otp-slot {
  cursor: not-allowed;
  opacity: 0.6;
}

/* ═══════════════════════════════════════════════════════════════
   REDUCED MOTION
   ═══════════════════════════════════════════════════════════════ */

@media (prefers-reduced-motion: reduce) {
  .ren-otp-slot {
    transition: none;
  }
}

/* ═══════════════════════════════════════════════════════════════
   MOBILE RESPONSIVENESS
   ═══════════════════════════════════════════════════════════════ */

@container ren-otp (max-width: 360px) {
  .ren-otp {
    gap: var(--space-1);

    & .ren-otp-slot {
      width: var(--size-sm);
      height: var(--size-sm);
      font-size: var(--text-sm);
    }

    & .ren-otp-separator {
      height: var(--size-sm);
      font-size: var(--text-sm);
      padding: 0 var(--space-0-5);
    }
  }
}


id: file:components/composites/ren-otp/ren-otp.js
type: javascript
name: ren-otp.js
path: components/composites/ren-otp/ren-otp.js
data: {}
body:
/* ═══════════════════════════════════════════════════════════════
   REN OTP (ONE-TIME PASSWORD) INPUT WEB COMPONENT
   ═══════════════════════════════════════════════════════════════
   A light-DOM web component for collecting OTP/PIN codes.
   Auto-creates input slots, manages focus transitions,
   handles paste events, and validates completion.

   Attributes:
   - length: Number of code slots (default: 6)
   - type: "numeric" or "alphanumeric" (default: "numeric")
   - disabled: Disable all slots

   Data Attributes:
   - data-valid: Set to mark as success
   - data-invalid: Set to mark as error
   - data-filled: Set on individual slots when they have content

   Events:
   - ren-change: Dispatched on any slot change { value: partial }
   - ren-complete: Dispatched when all slots filled { value: complete }

   Methods:
   - getValue(): Return current combined code
   - reset(): Clear all slots
   - focus(): Focus first slot
   ═══════════════════════════════════════════════════════════════ */

export class RenOtp extends HTMLElement {
  constructor() {
    super();

    /* ═══ STATE ═══ */
    this.length = 6;
    this.type = 'numeric';
    this.slots = [];

    /* ═══ BIND METHODS ═══ */
    this.handleSlotInput = this.handleSlotInput.bind(this);
    this.handleSlotKeydown = this.handleSlotKeydown.bind(this);
    this.handleSlotPaste = this.handleSlotPaste.bind(this);
    this.handleSlotFocus = this.handleSlotFocus.bind(this);
  }

  connectedCallback() {
    /* ═══ READ ATTRIBUTES ═══ */
    const lengthAttr = this.getAttribute('length');
    const typeAttr = this.getAttribute('type');
    const disabledAttr = this.hasAttribute('disabled');

    if (lengthAttr !== null) this.length = parseInt(lengthAttr, 10);
    if (typeAttr !== null) this.type = typeAttr;

    /* ═══ CREATE SLOTS ═══ */
    this.createSlots(disabledAttr);
  }

  /* ═══════════════════════════════════════════════════════════════
     SLOT CREATION & SETUP
     ═══════════════════════════════════════════════════════════════ */
  createSlots(disabled = false) {
    /* ═══ CLEAR EXISTING SLOTS ═══ */
    this.slots = [];
    const existingSlots = this.querySelectorAll('.ren-otp-slot');
    existingSlots.forEach((slot) => slot.remove());

    /* ═══ CREATE NEW SLOTS ═══ */
    for (let i = 0; i < this.length; i++) {
      const slot = document.createElement('input');
      slot.type = 'text';
      slot.className = 'ren-otp-slot';
      slot.setAttribute('inputmode', this.type === 'numeric' ? 'numeric' : 'text');
      slot.setAttribute('maxlength', '1');
      slot.setAttribute('placeholder', '•');
      slot.setAttribute('autocomplete', 'off');
      slot.setAttribute('data-index', i.toString());
      slot.setAttribute('aria-label', `Code digit ${i + 1} of ${this.length}`);

      if (disabled) {
        slot.disabled = true;
      }

      /* ═══ ATTACH EVENT LISTENERS ═══ */
      slot.addEventListener('input', this.handleSlotInput);
      slot.addEventListener('keydown', this.handleSlotKeydown);
      slot.addEventListener('paste', this.handleSlotPaste);
      slot.addEventListener('focus', this.handleSlotFocus);

      this.appendChild(slot);
      this.slots.push(slot);
    }
  }

  /* ═══════════════════════════════════════════════════════════════
     EVENT HANDLERS
     ═══════════════════════════════════════════════════════════════ */

  handleSlotInput = (e) => {
    const slot = e.target;
    const index = parseInt(slot.getAttribute('data-index'), 10);
    let value = slot.value;

    /* ═══ VALIDATE INPUT ═══ */
    if (this.type === 'numeric') {
      value = value.replace(/[^0-9]/g, '');
    } else if (this.type === 'alphanumeric') {
      value = value.replace(/[^a-zA-Z0-9]/g, '');
    }

    /* ═══ ENFORCE MAX LENGTH ═══ */
    if (value.length > 1) {
      value = value.slice(0, 1);
    }

    slot.value = value;

    /* ═══ UPDATE FILLED STATE ═══ */
    if (value) {
      slot.setAttribute('data-filled', '');
    } else {
      slot.removeAttribute('data-filled');
    }

    /* ═══ AUTO-ADVANCE TO NEXT SLOT ═══ */
    if (value && index < this.slots.length - 1) {
      this.slots[index + 1].focus();
    }

    /* ═══ DISPATCH CHANGE EVENT ═══ */
    this.dispatchChangeEvent();

    /* ═══ CHECK COMPLETION ═══ */
    this.checkCompletion();
  };

  handleSlotKeydown = (e) => {
    const slot = e.target;
    const index = parseInt(slot.getAttribute('data-index'), 10);

    /* ═══ BACKSPACE: MOVE TO PREVIOUS SLOT ═══ */
    if (e.key === 'Backspace') {
      if (slot.value) {
        slot.value = '';
        slot.removeAttribute('data-filled');
        this.dispatchChangeEvent();
      } else if (index > 0) {
        e.preventDefault();
        const prevSlot = this.slots[index - 1];
        prevSlot.value = '';
        prevSlot.removeAttribute('data-filled');
        prevSlot.focus();
        this.dispatchChangeEvent();
      }
    }

    /* ═══ ARROW KEYS: NAVIGATE SLOTS ═══ */
    if (e.key === 'ArrowLeft' && index > 0) {
      e.preventDefault();
      this.slots[index - 1].focus();
    } else if (e.key === 'ArrowRight' && index < this.slots.length - 1) {
      e.preventDefault();
      this.slots[index + 1].focus();
    }

    /* ═══ HOME/END: FIRST/LAST SLOT ═══ */
    if (e.key === 'Home') {
      e.preventDefault();
      this.slots[0].focus();
    } else if (e.key === 'End') {
      e.preventDefault();
      this.slots[this.slots.length - 1].focus();
    }
  };

  handleSlotPaste = (e) => {
    e.preventDefault();
    const pastedData = e.clipboardData?.getData('text') || '';

    if (!pastedData) return;

    /* ═══ VALIDATE PASTED DATA ═══ */
    let sanitized = pastedData;
    if (this.type === 'numeric') {
      sanitized = pastedData.replace(/[^0-9]/g, '');
    } else if (this.type === 'alphanumeric') {
      sanitized = pastedData.replace(/[^a-zA-Z0-9]/g, '');
    }

    /* ═══ DISTRIBUTE ACROSS SLOTS ═══ */
    const index = parseInt(e.target.getAttribute('data-index'), 10);
    for (let i = 0; i < sanitized.length && index + i < this.slots.length; i++) {
      const slot = this.slots[index + i];
      slot.value = sanitized[i];
      slot.setAttribute('data-filled', '');
    }

    /* ═══ FOCUS NEXT EMPTY OR LAST SLOT ═══ */
    const nextEmptyIndex = this.slots.findIndex(
      (s, i) => i >= index && s.value === ''
    );
    if (nextEmptyIndex !== -1) {
      this.slots[nextEmptyIndex].focus();
    } else {
      this.slots[this.slots.length - 1].focus();
    }

    /* ═══ DISPATCH CHANGE & CHECK COMPLETION ═══ */
    this.dispatchChangeEvent();
    this.checkCompletion();
  };

  handleSlotFocus = (e) => {
    /* ═══ SELECT ALL TEXT WHEN FOCUSED ═══ */
    e.target.select();
  };

  /* ═══════════════════════════════════════════════════════════════
     PUBLIC METHODS
     ═══════════════════════════════════════════════════════════════ */

  getValue() {
    return this.slots.map((slot) => slot.value).join('');
  }

  reset() {
    this.slots.forEach((slot) => {
      slot.value = '';
      slot.removeAttribute('data-filled');
    });

    this.removeAttribute('data-valid');
    this.removeAttribute('data-invalid');
    this.dispatchChangeEvent();

    if (this.slots.length > 0) {
      this.slots[0].focus();
    }
  }

  setValid(isValid) {
    if (isValid) {
      this.setAttribute('data-valid', '');
      this.removeAttribute('data-invalid');
    } else {
      this.setAttribute('data-invalid', '');
      this.removeAttribute('data-valid');
    }
  }

  clearValidation() {
    this.removeAttribute('data-valid');
    this.removeAttribute('data-invalid');
  }

  /* ═══════════════════════════════════════════════════════════════
     UTILITY METHODS
     ═══════════════════════════════════════════════════════════════ */

  checkCompletion() {
    const value = this.getValue();
    const isComplete = value.length === this.length;

    if (isComplete) {
      this.dispatchCompleteEvent(value);
    }
  }

  dispatchChangeEvent() {
    const value = this.getValue();

    this.dispatchEvent(
      new CustomEvent('ren-change', {
        bubbles: true,
        composed: true,
        detail: {
          value: value,
        },
      })
    );
  }

  dispatchCompleteEvent(value) {
    this.dispatchEvent(
      new CustomEvent('ren-complete', {
        bubbles: true,
        composed: true,
        detail: {
          value: value,
        },
      })
    );
  }

  /* ═══════════════════════════════════════════════════════════════
     PROPERTIES
     ═══════════════════════════════════════════════════════════════ */

  get disabled() {
    return this.hasAttribute('disabled');
  }

  set disabled(val) {
    if (val) {
      this.setAttribute('disabled', '');
      this.slots.forEach((slot) => {
        slot.disabled = true;
      });
    } else {
      this.removeAttribute('disabled');
      this.slots.forEach((slot) => {
        slot.disabled = false;
      });
    }
  }

  /* ═══ OVERRIDE FOCUS METHOD ═══ */
  focus() {
    if (this.slots.length > 0) {
      this.slots[0].focus();
    }
  }
}

/* ═══ REGISTER COMPONENT ═══ */
if (!customElements.get('ren-otp')) {
  customElements.define('ren-otp', RenOtp);
}


id: file:components/composites/ren-popover/ren-popover.css
type: css
name: ren-popover.css
path: components/composites/ren-popover/ren-popover.css
data: {}
body:
/* ═══ ANCHOR POSITIONING & BASE STYLES ═══ */
.ren-popover {
  position: absolute;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  padding: var(--space-4);
  max-width: 20rem;
  z-index: var(--z-popover, 1000);
  pointer-events: auto;
  margin: 0;
  inset: auto;
  border-top: none;

  /* Anchor positioning (modern browsers) */
  position-anchor: --popover-anchor;

  /* Default: below the trigger with automatic fallback */
  position-area: bottom span-all;

  /* Automatic flip when near viewport edges */
  position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;

  /* Enter/exit transitions driven by semantic motion tokens. */
  opacity: 0;
  transform: translateY(-4px) scale(0.95);
  transition:
    opacity   var(--duration-enter) var(--ease-enter),
    transform var(--duration-enter) var(--ease-enter),
    overlay   var(--duration-enter) var(--ease-enter) allow-discrete,
    display   var(--duration-enter) var(--ease-enter) allow-discrete;
}

/* ═══ POPOVER OPEN STATE ═══ */
.ren-popover:popover-open,
.ren-popover.ren-open {
  opacity: 1;
  transform: translateY(0) scale(1);
}

/* ═══ POPOVER OPEN WITH @starting-style ═══ */
@starting-style {
  .ren-popover:popover-open {
    opacity: 0;
    transform: translateY(-4px) scale(0.95);
  }
}

/* ═══ POPOVER ARROW/CARET ═══ */
.ren-popover-arrow {
  position: absolute;
  width: 8px;
  height: 8px;
  background: inherit;
  border: inherit;
  border-top: none;
  border-inline-start: none;
  transform: rotate(45deg);
  z-index: -1;
}

/* ═══ PLACEMENT VARIANTS ═══ */
/* Bottom (default) */
.ren-popover[data-side="bottom"],
.ren-popover:not([data-side]) {
  position-area: bottom span-all;
  margin-block-start: var(--space-2);
  margin-block-end: 0;
  margin-inline: 0;

  & .ren-popover-arrow {
    top: -4px;
    left: 50%;
    transform: translateX(-50%) rotate(225deg);
  }
}

/* Top */
.ren-popover[data-side="top"] {
  position-area: top span-all;
  margin-block-start: 0;
  margin-block-end: var(--space-2);
  margin-inline: 0;

  & .ren-popover-arrow {
    bottom: -4px;
    left: 50%;
    transform: translateX(-50%) rotate(45deg);
  }
}

/* Right */
.ren-popover[data-side="right"] {
  position-area: right span-all;
  margin-block: 0;
  margin-inline-start: var(--space-2);
  margin-inline-end: 0;

  & .ren-popover-arrow {
    left: -4px;
    top: 50%;
    transform: translateY(-50%) rotate(135deg);
  }
}

/* Left */
.ren-popover[data-side="left"] {
  position-area: left span-all;
  margin-block: 0;
  margin-inline-start: 0;
  margin-inline-end: var(--space-2);

  & .ren-popover-arrow {
    right: -4px;
    top: 50%;
    transform: translateY(-50%) rotate(315deg);
  }
}

/* ═══ POPOVER SECTIONS ═══ */
.ren-popover-header {
  margin-bottom: var(--space-2);
  font-weight: 600;
  color: var(--color-text-primary);
}

.ren-popover-body {
  color: var(--color-text-secondary);
  font-size: var(--size-body-sm);
  line-height: 1.5;
}

.ren-popover-footer {
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-border);
  display: flex;
  gap: var(--space-2);
}

/* ═══ RESPECTS MOTION PREFERENCES ═══
   Semantic --duration-enter already collapses to 0ms under
   reduced-motion. We keep this block to drop the transform
   (scale/translate) in the starting-style, so the popover
   fades in place instead of moving. */
@media (prefers-reduced-motion: reduce) {
  .ren-popover {
    transition:
      opacity var(--duration-enter),
      overlay var(--duration-enter) allow-discrete,
      display var(--duration-enter) allow-discrete;
    transform: none;
  }

  @starting-style {
    .ren-popover:popover-open {
      opacity: 0;
      transform: none;
    }
  }
}

/* ═══ FALLBACK FOR BROWSERS WITHOUT COMPLETE ANCHOR POSITIONING ═══ */
@supports not ((anchor-name: --ren-anchor) and
  (position-anchor: --ren-anchor) and
  (position-area: bottom span-all)) {
  .ren-popover {
    position: absolute;
    inset: auto auto auto auto;
  }

  [data-popover-trigger],
  .ren-popover-trigger {
    position: relative;
  }

  /* Fallback animations */
  .ren-popover {
    animation: ren-popover-fallback-open var(--duration-enter) var(--ease-enter);
  }

  .ren-popover:popover-open,
  .ren-popover.ren-open {
    animation: ren-popover-fallback-open var(--duration-enter) var(--ease-enter);
  }

  @media (prefers-reduced-motion: reduce) {
    .ren-popover {
      animation: none;
    }
  }
}

@keyframes ren-popover-fallback-open {
  from {
    opacity: 0;
    transform: translateY(-4px) scale(0.95);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

/* Appearance API bridge */
.ren-popover {
  width: var(--ren-popover-width, 280px);
  max-width: var(--ren-popover-width, 280px);
  padding: var(--ren-popover-padding, var(--space-4));
  border-radius: var(--ren-popover-radius, var(--radius-lg));
  background: var(--ren-popover-bg, var(--color-surface-overlay));
  border-color: var(--ren-popover-border-color, var(--color-border));
  box-shadow: var(--ren-popover-shadow, var(--shadow-lg));
  transition-duration: var(--ren-popover-duration, var(--duration-normal));
  transition-timing-function: var(--ren-popover-easing, var(--ease-out));
}


id: file:components/composites/ren-popover/ren-popover.js
type: javascript
name: ren-popover.js
path: components/composites/ren-popover/ren-popover.js
data: {}
body:
let nextPopoverId = 0;
const FOCUSABLE_SELECTOR = [
  'a[href]',
  'button:not([disabled])',
  'input:not([disabled]):not([type="hidden"])',
  'select:not([disabled])',
  'textarea:not([disabled])',
  '[tabindex]:not([tabindex="-1"])',
].join(', ');
const PLACEMENTS = new Set(['top', 'right', 'bottom', 'left']);

function supportsAnchorPositioning() {
  return (
    typeof CSS !== 'undefined' &&
    typeof CSS.supports === 'function' &&
    CSS.supports('anchor-name', '--ren-anchor') &&
    CSS.supports('position-anchor', '--ren-anchor') &&
    CSS.supports('position-area', 'bottom span-all')
  );
}

function normalizePlacement(value, fallback = 'bottom') {
  return PLACEMENTS.has(value) ? value : fallback;
}

/**
 * Fallback position computation for browsers without CSS anchor positioning.
 * Used only when full CSS anchor positioning support is unavailable.
 *
 * @param {HTMLElement} trigger - The trigger element
 * @param {HTMLElement} popover - The popover element
 * @param {string} placement - Placement: 'top', 'right', 'bottom', 'left'
 * @param {number} offset - Offset in pixels between trigger and popover
 * @returns {Object} Position object with x, y, and finalPlacement properties
 */
function computePosition(trigger, popover, placement = 'bottom', offset = 8) {
  const triggerRect = trigger.getBoundingClientRect();
  const popoverRect = popover.getBoundingClientRect();
  const viewport = { width: window.innerWidth, height: window.innerHeight };

  let x = 0;
  let y = 0;
  let finalPlacement = placement;

  const placements = {
    top: () => {
      x = triggerRect.left + (triggerRect.width - popoverRect.width) / 2;
      y = triggerRect.top - popoverRect.height - offset;
      if (y < 0) {
        finalPlacement = 'bottom';
        return placements.bottom();
      }
      return { x, y };
    },
    bottom: () => {
      x = triggerRect.left + (triggerRect.width - popoverRect.width) / 2;
      y = triggerRect.bottom + offset;
      if (y + popoverRect.height > viewport.height) {
        finalPlacement = 'top';
        return placements.top();
      }
      return { x, y };
    },
    left: () => {
      x = triggerRect.left - popoverRect.width - offset;
      y = triggerRect.top + (triggerRect.height - popoverRect.height) / 2;
      if (x < 0) {
        finalPlacement = 'right';
        return placements.right();
      }
      return { x, y };
    },
    right: () => {
      x = triggerRect.right + offset;
      y = triggerRect.top + (triggerRect.height - popoverRect.height) / 2;
      if (x + popoverRect.width > viewport.width) {
        finalPlacement = 'left';
        return placements.left();
      }
      return { x, y };
    },
  };

  const result = placements[placement]?.() || placements.bottom();

  // Clamp X position within viewport
  if (result.x < 0) result.x = 8;
  else if (result.x + popoverRect.width > viewport.width)
    result.x = viewport.width - popoverRect.width - 8;

  return { ...result, finalPlacement };
}

/**
 * Popover component with CSS Anchor Positioning and native Popover API
 *
 * Displays content relative to a trigger element with automatic viewport collision
 * handling via CSS anchor positioning. Falls back to JS positioning in unsupported browsers.
 *
 * @example
 * <button data-popover-trigger>Open Popover</button>
 * <ren-popover placement="bottom" offset="8">
 *   <div class="ren-popover-header">Title</div>
 *   <div class="ren-popover-body">Content</div>
 * </ren-popover>
 *
 * @fires ren-open - Fired when popover opens
 * @fires ren-close - Fired when popover closes
 */
export class RenPopover extends HTMLElement {
  static observedAttributes = ['placement'];
  static supportsAnchor = supportsAnchorPositioning();

  #trigger = null;
  #triggerController = null;
  #dismissController = null;

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'placement' && oldValue !== newValue) {
      this.#syncPlacement();
    }
  }

  connectedCallback() {
    this.setupPopover();
    this.findTrigger();
    this.attachTriggerListener();
  }

  disconnectedCallback() {
    this.cleanup();
  }

  /**
   * Setup popover element with required attributes
   * @private
   */
  setupPopover() {
    this.classList.add('ren-popover');

    if (!this.id) {
      this.id = `ren-popover-${++nextPopoverId}`;
    }

    // Add arrow if not present
    if (!this.querySelector('.ren-popover-arrow')) {
      const arrow = document.createElement('div');
      arrow.className = 'ren-popover-arrow';
      this.appendChild(arrow);
    }

    // Setup native Popover API
    if ('popover' in HTMLElement.prototype) {
      this.setAttribute('popover', 'manual');
    }

    // Set accessibility attributes
    this.setAttribute('role', 'dialog');
    this.setAttribute('aria-modal', 'false');
    this.#syncPlacement();
  }

  /**
   * Find the trigger element
   * @private
   */
  findTrigger() {
    // Explicit trigger ID
    const triggerId = this.getAttribute('trigger-id');
    if (triggerId) {
      this.#trigger = document.getElementById(triggerId);
    }

    // Query selector for data-popover-trigger
    if (!this.#trigger) {
      this.#trigger = this.querySelector('[data-popover-trigger]');
    }

    // Fall back to previous sibling
    if (!this.#trigger) {
      this.#trigger = this.previousElementSibling;
    }
  }

  /**
   * Attach trigger listener and setup anchor relationship
   * @private
   */
  attachTriggerListener() {
    if (!this.#trigger) return;

    this.#triggerController?.abort();
    this.#triggerController = new AbortController();

    // Set up anchor relationship if CSS anchors are supported
    if (RenPopover.supportsAnchor) {
      this.#trigger.style.anchorName = '--popover-anchor';
    } else {
      // Fallback: ensure trigger can be positioned relative to
      if (getComputedStyle(this.#trigger.parentElement).position === 'static') {
        this.#trigger.parentElement.style.position = 'relative';
      }
    }

    // Wire up popovertarget if not already set
    if (!this.#trigger.hasAttribute('popovertarget')) {
      this.#trigger.setAttribute('popovertarget', this.id);
    }
    this.#trigger.setAttribute('aria-haspopup', 'dialog');
    this.#trigger.setAttribute('aria-controls', this.id);
    this.#trigger.setAttribute('aria-expanded', this.isOpen() ? 'true' : 'false');

    // Click handler
    this.#trigger.addEventListener(
      'click',
      (e) => {
        e.preventDefault();
        e.stopPropagation();
        this.toggle();
      },
      { signal: this.#triggerController.signal }
    );

    // Setup dismiss behavior (click outside, Escape key)
    this.#dismissController?.abort();
    this.#dismissController = new AbortController();

    document.addEventListener(
      'click',
      (e) => {
        if (
          this.isOpen() &&
          e.target !== this &&
          !this.contains(e.target) &&
          e.target !== this.#trigger &&
          !this.#trigger?.contains(e.target)
        ) {
          this.close();
        }
      },
      { signal: this.#dismissController.signal }
    );

    document.addEventListener(
      'keydown',
      (e) => {
        if (e.key === 'Escape' && this.isOpen()) {
          this.close();
        }
      },
      { signal: this.#dismissController.signal }
    );
  }

  /**
   * Position popover relative to trigger (fallback for non-anchor browsers)
   * @private
   */
  positionPopover() {
    if (!this.#trigger || RenPopover.supportsAnchor) return;

    const placement = normalizePlacement(this.getAttribute('placement'), 'bottom');
    const offset = parseInt(this.getAttribute('offset')) || 8;

    const { x, y, finalPlacement } = computePosition(
      this.#trigger,
      this,
      placement,
      offset
    );

    this.style.left = `${x}px`;
    this.style.top = `${y}px`;
    this.setAttribute('data-side', finalPlacement);
  }

  #syncPlacement() {
    const placement = normalizePlacement(this.getAttribute('placement'), 'bottom');
    this.setAttribute('data-side', placement);
  }

  /**
   * Open the popover
   */
  open() {
    if (this.isOpen()) return;

    this.positionPopover();
    this.setAttribute('data-state', 'open');

    if ('popover' in HTMLElement.prototype) {
      try {
        this.showPopover();
      } catch {
        // Already open or other error
      }
    } else {
      this.classList.add('ren-open');
    }

    // Popover is explicitly non-modal (see component.md).
    // No aria-modal or focus trap — those are for ren-dialog / ren-sheet.
    this.setAttribute('aria-modal', 'false');
    this.#trigger?.setAttribute('aria-expanded', 'true');
    // Note: no focus trap for non-modal popover.
    requestAnimationFrame(() => this.#focusInitialElement());
    this.dispatchEvent(new CustomEvent('ren-open', { bubbles: true }));
  }

  /**
   * Close the popover
   */
  close() {
    if (!this.isOpen()) return;

    const activeElement = document.activeElement;
    const shouldRestoreFocus =
      this.#trigger &&
      activeElement instanceof HTMLElement &&
      this.contains(activeElement);

    this.setAttribute('data-state', 'closed');

    if ('popover' in HTMLElement.prototype) {
      try {
        this.hidePopover();
      } catch {
        // Already closed or other error
      }
    } else {
      this.classList.remove('ren-open');
    }

    this.setAttribute('aria-modal', 'false');
    this.#trigger?.setAttribute('aria-expanded', 'false');
    if (shouldRestoreFocus) {
      this.#trigger.focus({ preventScroll: true });
    }
    this.dispatchEvent(new CustomEvent('ren-close', { bubbles: true }));
  }

  /**
   * Toggle popover open/closed state
   */
  toggle() {
    if (this.isOpen()) {
      this.close();
    } else {
      this.open();
    }
  }

  /**
   * Check if popover is currently open
   * @returns {boolean}
   */
  isOpen() {
    if ('popover' in HTMLElement.prototype) {
      return this.matches(':popover-open');
    }
    return this.classList.contains('ren-open');
  }

  /**
   * Cleanup event listeners
   * @private
   */
  cleanup() {
    this.#triggerController?.abort();
    this.#triggerController = null;
    this.#dismissController?.abort();
    this.#dismissController = null;
  }

  #focusInitialElement() {
    const target = this.querySelector(FOCUSABLE_SELECTOR) || this;
    if (target === this && !this.hasAttribute('tabindex')) {
      this.setAttribute('tabindex', '-1');
    }
    target.focus({ preventScroll: true });
  }

  /**
   * Get the trigger element
   * @returns {HTMLElement|null}
   */
  getTrigger() {
    return this.#trigger;
  }

  /**
   * Set the trigger element
   * @param {HTMLElement} trigger
   */
  setTrigger(trigger) {
    this.#trigger = trigger;
    this.attachTriggerListener();
  }
}

if (!customElements.get('ren-popover')) {
  customElements.define('ren-popover', RenPopover);
}


id: file:components/composites/ren-scroll-area/ren-scroll-area.css
type: css
name: ren-scroll-area.css
path: components/composites/ren-scroll-area/ren-scroll-area.css
data: {}
body:
.ren-scroll-area {
  position: relative;
  overflow: auto;

  scrollbar-width: thin;
  scrollbar-color: var(--color-fill-active) transparent;

  transition: scrollbar-color var(--duration-enter) var(--ease-enter);

  &::-webkit-scrollbar {
    width: 8px;
    height: 8px;
  }

  &::-webkit-scrollbar-track {
    background: transparent;
  }

  &::-webkit-scrollbar-thumb {
    background: var(--color-fill-active);
    border-radius: var(--radius-full);
    border: 2px solid transparent;
    background-clip: content-box;

    transition: var(--transition-tactile);
  }

  &::-webkit-scrollbar-thumb:hover {
    background-color: var(--color-fill-hover);
    background-clip: content-box;
  }

  &::-webkit-scrollbar-corner {
    background: transparent;
  }

  /* Hover state for standard scrollbar */
  &:hover {
    scrollbar-color: var(--color-fill-hover) transparent;
  }

  /* Variants */
  &.-auto {
    scrollbar-width: none;

    &::-webkit-scrollbar {
      display: none;
    }

    /* Show on scroll */
    &:is(:hover, :focus-within) {
      scrollbar-width: thin;
      scrollbar-color: var(--color-fill-active) transparent;

      &::-webkit-scrollbar {
        display: block;
      }
    }
  }

  &.-always {
    scrollbar-width: thin;
    scrollbar-color: var(--color-fill-active) transparent;

    &::-webkit-scrollbar {
      display: block;
    }
  }

  &.-hidden {
    scrollbar-width: none;

    &::-webkit-scrollbar {
      display: none;
    }
  }

  /* Direction variants */
  &.-x {
    overflow-x: auto;
    overflow-y: hidden;
  }

  &.-y {
    overflow-x: hidden;
    overflow-y: auto;
  }

  /* Fade edges for smooth content transitions */
  &.-fade {
    position: relative;

    &::before {
      content: '';
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      height: 20px;

      pointer-events: none;
      background: linear-gradient(
        to bottom,
        var(--color-surface) 0%,
        transparent 100%
      );

      z-index: 10;
    }

    &::after {
      content: '';
      position: absolute;
      bottom: 0;
      left: 0;
      right: 0;
      height: 20px;

      pointer-events: none;
      background: linear-gradient(
        to top,
        var(--color-surface) 0%,
        transparent 100%
      );

      z-index: 10;
    }
  }

  /* Horizontal fade */
  &.-fade-x {
    position: relative;

    &::before {
      content: '';
      position: absolute;
      top: 0;
      left: 0;
      bottom: 0;
      width: 20px;

      pointer-events: none;
      background: linear-gradient(
        to right,
        var(--color-surface) 0%,
        transparent 100%
      );

      z-index: 10;
    }

    &::after {
      content: '';
      position: absolute;
      top: 0;
      right: 0;
      bottom: 0;
      width: 20px;

      pointer-events: none;
      background: linear-gradient(
        to left,
        var(--color-surface) 0%,
        transparent 100%
      );

      z-index: 10;
    }
  }

  /* Max-height with custom property */
  &[--scroll-max] {
    max-height: var(--scroll-max, 20rem);
  }

  /* Smooth scrolling behavior */
  scroll-behavior: smooth;

  @media (prefers-reduced-motion: reduce) {
    scroll-behavior: auto;
  }
}

/* Utility class for common heights */
.ren-scroll-area-sm {
  max-height: 12rem;
}

.ren-scroll-area-md {
  max-height: 20rem;
}

.ren-scroll-area-lg {
  max-height: 30rem;
}

.ren-scroll-area-xl {
  max-height: 40rem;
}

.ren-scroll-area-full {
  height: 100%;
  max-height: none;
}


id: file:components/composites/ren-select/ren-select.css
type: css
name: ren-select.css
path: components/composites/ren-select/ren-select.css
data: {}
body:
/* ═══════════════════════════════════════════════════════════════════
   RenDS — Select Component
   ═══════════════════════════════════════════════════════════════════
   Custom, accessible select/dropdown component with full keyboard nav,
   ARIA support, and multi-select capability.

   Structure:
   .ren-select (container)
     .ren-select-trigger (button showing selected value)
       .ren-select-value (the displayed text)
       .ren-select-placeholder (when nothing selected)
       .ren-select-icon (chevron)
     .ren-select-content (dropdown listbox)
       .ren-select-item (option)
       .ren-select-group (optional grouping)
       .ren-select-label (group header)
       .ren-select-separator (divider between groups)
   ═══════════════════════════════════════════════════════════════════ */

/* ═══ SELECT CONTAINER ═══ */
.ren-select {
  position: relative;
  display: inline-flex;
  flex-direction: column;
  width: 100%;
  font-family: inherit;
}

/* ═══ SELECT TRIGGER (Button) ═══ */
.ren-select-trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);

  /* Input styling */
  width: 100%;
  min-height: var(--touch-min);
  padding: var(--space-2) var(--space-3);
  font-size: var(--body-size);
  font-family: inherit;
  color: var(--color-text);
  background-color: var(--color-input-bg);
  border: var(--stroke-1) solid var(--color-input-border);
  border-radius: var(--radius-md);
  cursor: pointer;
  text-align: start;
  white-space: nowrap;

  /* Semantic preset keeps trigger hover/focus in sync with other
     tactile inputs (button, checkbox, text field). */
  transition: var(--transition-tactile);
}

.ren-select-trigger:hover:not(:disabled):not([aria-expanded="true"]) {
  background-color: var(--color-input-bg-hover);
}

.ren-select-trigger:focus-visible {
  outline: none;
  border-color: var(--color-input-border-focus);
  box-shadow: 0 0 0 3px var(--color-input-focus-ring);
  background-color: var(--color-input-bg-hover);
}

.ren-select-trigger[aria-expanded="true"] {
  border-color: var(--color-input-border-focus);
  box-shadow: 0 0 0 3px var(--color-input-focus-ring);
  background-color: var(--color-input-bg-hover);
}

.ren-select-trigger:disabled {
  background-color: var(--color-input-disabled-bg);
  color: var(--color-input-disabled-text);
  cursor: not-allowed;
  opacity: 0.7;
}

/* ═══ SELECT VALUE / PLACEHOLDER ═══ */
.ren-select-value {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--color-text);
}

.ren-select-placeholder {
  color: var(--color-input-placeholder);
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ═══ SELECT ICON (Chevron) ═══ */
.ren-select-icon {
  flex-shrink: 0;
  width: 1.25rem;
  height: 1.25rem;
  color: var(--color-text-muted);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: transform var(--duration-micro) var(--ease-enter);
  pointer-events: none;
}

.ren-select-trigger[aria-expanded="true"] .ren-select-icon {
  transform: rotate(180deg);
}

/* SVG chevron icon styling */
.ren-select-icon svg {
  width: 100%;
  height: 100%;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* ═══ SELECT CONTENT (Dropdown Listbox) ═══ */
.ren-select-content {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  margin-top: var(--space-1);
  background: var(--color-surface);
  border: var(--stroke-1) solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  padding-block: var(--space-2);
  max-height: 15rem;
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior: contain;
  z-index: var(--z-dropdown, 1000);

  visibility: hidden;
  opacity: 0;
  transform: translateY(-4px) scale(0.98);
  animation: ren-select-close var(--duration-exit) var(--ease-exit) forwards;
}

/* ═══ SELECT CONTENT OPEN STATE ═══ */
.ren-select-content[popover]:popover-open,
.ren-select-content.ren-open {
  visibility: visible;
  opacity: 1;
  animation: ren-select-open var(--duration-enter) var(--ease-enter);
}

@keyframes ren-select-open {
  from {
    opacity: 0;
    transform: translateY(-4px) scale(0.98);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

@keyframes ren-select-close {
  from {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
  to {
    opacity: 0;
    transform: translateY(-4px) scale(0.98);
  }
}

/* ═══ SELECT ITEM (Option) ═══ */
.ren-select-item {
  padding: var(--space-2) var(--space-3);
  display: flex;
  align-items: center;
  gap: var(--space-2);
  cursor: pointer;
  color: var(--color-text);
  border-radius: var(--radius-sm);
  /* Micro-duration keeps option hover snappy; tokens/semantic/motion
     collapses this to 0ms under reduced-motion. */
  transition:
    background-color var(--duration-micro) var(--ease-enter),
    color var(--duration-micro) var(--ease-enter);
  white-space: nowrap;
  user-select: none;
  min-height: 2rem;
}

.ren-select-item:hover:not([aria-disabled="true"]),
.ren-select-item[data-highlighted] {
  background-color: var(--color-fill);
  color: var(--color-text);
}

.ren-select-item[aria-selected="true"] {
  background-color: var(--color-accent-subtle);
  color: var(--color-accent);
  font-weight: 500;
}

.ren-select-item[aria-selected="true"]::before {
  content: '';
  flex-shrink: 0;
  width: 1.25rem;
  height: 1.25rem;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--color-accent);
  border-radius: var(--radius-sm);
  position: relative;
}

.ren-select-item[aria-selected="true"]::before {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: center;
  background-size: 70%;
}

.ren-select-item[aria-disabled="true"] {
  opacity: 0.5;
  cursor: not-allowed;
  pointer-events: none;
}

/* ═══ SELECT GROUP ═══ */
.ren-select-group {
  display: block;
  padding-block: var(--space-1);
}

/* ═══ SELECT GROUP LABEL ═══ */
.ren-select-label {
  padding: var(--space-1) var(--space-3);
  font-size: var(--caption-size);
  font-weight: 600;
  color: var(--color-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  white-space: nowrap;
  user-select: none;
}

/* ═══ SELECT SEPARATOR ═══ */
.ren-select-separator {
  height: var(--stroke-1);
  background: var(--color-border);
  margin-block: var(--space-1);
}

/* ═══ SELECT EMPTY STATE ═══ */
.ren-select-empty {
  padding: var(--space-3) var(--space-4);
  text-align: center;
  color: var(--color-text-muted);
  font-size: var(--body-size);
  white-space: nowrap;
}

/* ═══ SIZE VARIANTS ═══ */
.ren-select-sm .ren-select-trigger {
  min-height: var(--size-sm);
  padding: var(--space-1) var(--space-2);
  font-size: var(--text-sm);
  border-radius: var(--radius-sm);
}

.ren-select-lg .ren-select-trigger {
  min-height: var(--size-xl);
  padding: var(--space-3) var(--space-4);
  font-size: var(--text-lg);
}

/* ═══ MULTI-SELECT CHIPS/TAGS ═══ */
.ren-select-chips {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1);
  width: 100%;
}

.ren-select-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-1) var(--space-2);
  background: var(--color-fill);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  color: var(--color-text);
  white-space: nowrap;
}

.ren-select-chip-remove {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1rem;
  height: 1rem;
  cursor: pointer;
  color: var(--color-text-muted);
  transition: var(--transition-tactile);
}

.ren-select-chip-remove:hover {
  color: var(--color-text);
}

/* ═══ REDUCED MOTION ═══ */
@media (prefers-reduced-motion: reduce) {
  .ren-select-trigger,
  .ren-select-icon,
  .ren-select-item {
    transition: none;
  }

  .ren-select-content {
    animation: none;
    opacity: 0;
  }

  .ren-select-content.ren-open {
    opacity: 1;
  }
}

/* ═══ SCROLLBAR STYLING ═══ */
.ren-select-content::-webkit-scrollbar {
  width: 6px;
}

.ren-select-content::-webkit-scrollbar-track {
  background: transparent;
}

.ren-select-content::-webkit-scrollbar-thumb {
  background: var(--color-border);
  border-radius: 3px;
}

.ren-select-content::-webkit-scrollbar-thumb:hover {
  background: var(--color-text-muted);
}

/* ═══ NATIVE POPOVER API SUPPORT ═══ */
@supports (selector(:popover-open)) {
  .ren-select-content[popover] {
    visibility: visible;
    opacity: 0;
    transition: opacity var(--duration-enter) var(--ease-enter),
                overlay var(--duration-enter) var(--ease-enter) allow-discrete,
                display var(--duration-enter) var(--ease-enter) allow-discrete;
    animation: none;
  }

  @starting-style {
    .ren-select-content[popover]:popover-open {
      opacity: 0;
    }
  }

  .ren-select-content[popover]:popover-open {
    opacity: 1;
  }
}


id: file:components/composites/ren-select/ren-select.js
type: javascript
name: ren-select.js
path: components/composites/ren-select/ren-select.js
data: {}
body:
/**
 * RenDS — Select Component
 * ========================
 *
 * A custom, fully accessible select/dropdown component that extends native
 * form capabilities with keyboard navigation, ARIA support, grouping,
 * and multi-select capability.
 *
 * Features:
 * - Keyboard navigation (arrow keys, Enter, Space, Escape, Typeahead)
 * - Full ARIA support (combobox, listbox, role="option")
 * - Click-outside dismissal with roving tabindex
 * - Optional multi-select mode
 * - Option groups and separators
 * - Form submission integration
 * - Preferred placement with viewport-aware positioning
 * - Fallback styling for native select elements
 * - Respects prefers-reduced-motion
 *
 * @example
 * <ren-select placeholder="Choose an option" name="country">
 *   <button data-select-trigger>Select a country</button>
 *   <div data-select-content>
 *     <div data-select-item data-value="us">United States</div>
 *     <div data-select-item data-value="ca">Canada</div>
 *   </div>
 * </ren-select>
 *
 * @fires ren-select-change - Dispatched when selection changes
 * @fires change - Standard change event for form submission
 */

import { createKeyboardNav } from '../../../utils/keyboard-nav.js';
import { createDismissable } from '../../../utils/dismissable.js';
import { autoId } from '../../../utils/id-generator.js';

const SELECT_SIDES = new Set(['top', 'right', 'bottom', 'left']);
const SELECT_ALIGNS = new Set(['start', 'end']);

function normalizeSelectPlacement(value) {
  const [sideValue, alignValue] = String(value || 'bottom')
    .toLowerCase()
    .split('-');
  const side = SELECT_SIDES.has(sideValue) ? sideValue : 'bottom';
  const align = SELECT_ALIGNS.has(alignValue) ? alignValue : 'start';

  return { side, align };
}

/**
 * Compute dropdown position relative to trigger element.
 * Falls back above if not enough space below.
 *
 * @private
 * @param {HTMLElement} trigger - Trigger button element
 * @param {HTMLElement} content - Dropdown content element
 * @param {Object} placement - Normalized placement object
 * @returns {Object} Position with top, left, flipped, side, and align properties
 */
function computePosition(trigger, content, placement = normalizeSelectPlacement()) {
  const triggerRect = trigger.getBoundingClientRect();
  const contentRect = content.getBoundingClientRect();
  const viewport = {
    width: window.innerWidth,
    height: window.innerHeight,
  };
  const gap = 8;
  const margin = 16;

  let finalSide = placement.side;
  let flipped = false;
  let top = triggerRect.bottom + gap;
  let left = triggerRect.left;

  if (placement.align === 'end') {
    left = triggerRect.right - contentRect.width;
  }

  if (placement.side === 'top') {
    top = triggerRect.top - contentRect.height - gap;
  } else if (placement.side === 'right') {
    top = placement.align === 'end'
      ? triggerRect.bottom - contentRect.height
      : triggerRect.top;
    left = triggerRect.right + gap;
  } else if (placement.side === 'left') {
    top = placement.align === 'end'
      ? triggerRect.bottom - contentRect.height
      : triggerRect.top;
    left = triggerRect.left - contentRect.width - gap;
  }

  if (
    placement.side === 'bottom' &&
    top + contentRect.height > viewport.height - margin &&
    triggerRect.top - contentRect.height - gap >= margin
  ) {
    top = triggerRect.top - contentRect.height - gap;
    finalSide = 'top';
    flipped = true;
  } else if (
    placement.side === 'top' &&
    top < margin &&
    triggerRect.bottom + contentRect.height + gap <= viewport.height - margin
  ) {
    top = triggerRect.bottom + gap;
    finalSide = 'bottom';
    flipped = true;
  } else if (
    placement.side === 'right' &&
    left + contentRect.width > viewport.width - margin &&
    triggerRect.left - contentRect.width - gap >= margin
  ) {
    left = triggerRect.left - contentRect.width - gap;
    finalSide = 'left';
    flipped = true;
  } else if (
    placement.side === 'left' &&
    left < margin &&
    triggerRect.right + contentRect.width + gap <= viewport.width - margin
  ) {
    left = triggerRect.right + gap;
    finalSide = 'right';
    flipped = true;
  }

  if (left + contentRect.width > viewport.width - margin) {
    left = viewport.width - contentRect.width - margin;
  }
  if (left < margin) {
    left = margin;
  }

  if (top + contentRect.height > viewport.height - margin) {
    top = viewport.height - contentRect.height - margin;
  }
  if (top < margin) {
    top = margin;
  }

  return { top, left, flipped, side: finalSide, align: placement.align };
}

/**
 * Select Component
 *
 * Custom, accessible select/dropdown with keyboard nav and ARIA support.
 *
 * @class RenSelect
 * @extends HTMLElement
 */
export class RenSelect extends HTMLElement {
  static observedAttributes = ['placement'];

  #trigger = null;
  #content = null;
  #items = [];
  #isOpen = false;
  #selectedValue = null;
  #selectedItem = null;
  #keyboardNav = null;
  #dismissable = null;
  #hiddenInputs = [];
  #animationFrame = null;
  #listenerController = null;
  #scrollController = null;

  constructor() {
    super();
    this.positionContent = this.positionContent.bind(this);
  }

  /* ─────────────────────────────────────────────────────────────
     LIFECYCLE
     ───────────────────────────────────────────────────────────── */

  connectedCallback() {
    if (this.hasAttribute('multiple') && !Array.isArray(this.#selectedValue)) {
      this.#selectedValue = [];
    }
    this.loadStyles();
    this.setupComponent();
    this.bindElements();
    this.setupARIA();
    this.setupHiddenInput();
    this.attachListeners();
  }

  disconnectedCallback() {
    this.cleanup();
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'placement' && oldValue !== newValue) {
      this.syncPlacement();
      if (this.#isOpen) {
        this.positionContent();
      }
    }
  }

  /* ─────────────────────────────────────────────────────────────
     SETUP & INITIALIZATION
     ───────────────────────────────────────────────────────────── */

  /**
   * Load component CSS styles into document
   * @private
   */
  loadStyles() {
    if (!document.getElementById('ren-select-styles')) {
      const style = document.createElement('style');
      style.id = 'ren-select-styles';
      style.textContent = this.constructor.styles || '';
      document.head.appendChild(style);
    }
  }

  /**
   * Setup container and trigger elements
   * @private
   */
  setupComponent() {
    this.classList.add('ren-select');
    this.syncPlacement();

    // Check for size variant
    if (this.hasAttribute('size')) {
      this.classList.add(`ren-select-${this.getAttribute('size')}`);
    }
  }

  /**
   * Find and cache trigger and content elements
   * @private
   */
  bindElements() {
    // Try to find explicitly marked elements
    this.#trigger = this.querySelector('[data-select-trigger], .ren-select-trigger');
    this.#content = this.querySelector('[data-select-content], .ren-select-content');

    // If not found, look for first button and first div (fallback)
    if (!this.#trigger) {
      this.#trigger = this.querySelector('button');
    }

    if (!this.#content) {
      this.#content = this.querySelector('[role="listbox"]');
    }

    // If still not found, create default structure
    if (!this.#trigger || !this.#content) {
      this.createDefaultStructure();
    }

    // Cache items
    this.updateItems();
    this.syncPlacement();
  }

  /**
   * Create default trigger and content if not provided
   * @private
   */
  createDefaultStructure() {
    // Create trigger button
    if (!this.#trigger) {
      this.#trigger = document.createElement('button');
      this.#trigger.className = 'ren-select-trigger';
      this.#trigger.type = 'button';
      this.appendChild(this.#trigger);
    }

    // Create content container
    if (!this.#content) {
      this.#content = document.createElement('div');
      this.#content.className = 'ren-select-content';
      this.#content.setAttribute('role', 'listbox');
      this.appendChild(this.#content);
    }
  }

  /**
   * Setup ARIA attributes on trigger and content
   * @private
   */
  setupARIA() {
    autoId(this.#trigger, 'select-trigger');
    const contentId = autoId(this.#content, 'select-content');

    // Trigger setup
    this.#trigger.setAttribute('role', 'combobox');
    this.#trigger.setAttribute('aria-expanded', 'false');
    this.#trigger.setAttribute('aria-haspopup', 'listbox');
    this.#trigger.setAttribute('aria-controls', contentId);
    this.#trigger.type = 'button';

    // Content setup
    this.#content.setAttribute('role', 'listbox');
    this.#content.id = contentId;
    if (this.hasAttribute('multiple')) {
      this.#content.setAttribute('aria-multiselectable', 'true');
    }

    // Set aria-label on content based on trigger text or placeholder
    const placeholder = this.getAttribute('placeholder') || 'Select an option';
    if (!this.#content.getAttribute('aria-label')) {
      this.#content.setAttribute('aria-label', placeholder);
    }

    // Wire up items
    this.updateItemsARIA();
  }

  /**
   * Update items ARIA attributes
   * @private
   */
  updateItemsARIA() {
    this.#items.forEach((item, index) => {
      if (item.hasAttribute('disabled') || item.getAttribute('aria-disabled') === 'true') {
        item.setAttribute('aria-disabled', 'true');
      }

      if (!item.getAttribute('role')) {
        item.setAttribute('role', 'option');
      }

      // Ensure item has tabindex
      if (!item.hasAttribute('tabindex')) {
        item.setAttribute('tabindex', '-1');
      }

      // Set aria-selected based on current selection
      const value = item.getAttribute('data-value');
      const isSelected = this.hasAttribute('multiple')
        ? this.#selectedValue.includes(value)
        : value === this.#selectedValue;
      item.setAttribute('aria-selected', isSelected ? 'true' : 'false');
    });
  }

  /**
   * Setup hidden input for form submission
   * @private
   */
  setupHiddenInput() {
    const name = this.getAttribute('name');
    if (!name) return;

    this.syncHiddenInputs();
  }

  /**
   * Attach event listeners to trigger and content
   * @private
   */
  attachListeners() {
    this.#listenerController?.abort();
    this.#listenerController = new AbortController();
    const { signal } = this.#listenerController;

    // Trigger click
    this.#trigger.addEventListener('click', (e) => this.handleTriggerClick(e), { signal });

    // Keyboard on trigger
    this.#trigger.addEventListener('keydown', (e) => this.handleTriggerKeyDown(e), { signal });

    // Item selection
    this.#content.addEventListener('click', (e) => this.handleItemClick(e), { signal });

    // Setup keyboard navigation in content
    this.setupKeyboardNav();

    // Setup click-outside dismissal
    this.setupDismissable();

    // Initialize selected value
    const initialValue = this.getAttribute('value');
    if (initialValue) {
      this.selectValue(initialValue, false);
    }
  }

  /**
   * Setup keyboard navigation for select items
   * @private
   */
  setupKeyboardNav() {
    this.#keyboardNav = createKeyboardNav(this.#content, {
      selector: '[role="option"]',
      orientation: 'vertical',
      loop: true,
      typeahead: true,
      focusOnHover: true,
      onActivate: (item) => {
        // Item is highlighted
      },
      onSelect: (item) => {
        this.selectItem(item);
      },
    });

    // Don't attach yet; will attach when dropdown opens
  }

  /**
   * Setup click-outside and Escape key dismissal
   * @private
   */
  setupDismissable() {
    this.#dismissable = createDismissable(this.#content, {
      triggerElement: this.#trigger,
      escapeKey: true,
      clickOutside: true,
      onDismiss: (reason) => {
        this.close();
      },
    });
  }

  /**
   * Update the list of items from the content element
   * @private
   */
  updateItems() {
    this.#items = Array.from(
      this.#content.querySelectorAll('[role="option"], [data-select-item], .ren-select-item')
    );
  }

  /* ─────────────────────────────────────────────────────────────
     EVENT HANDLERS
     ───────────────────────────────────────────────────────────── */

  /**
   * Handle trigger button click
   * @private
   */
  handleTriggerClick(e) {
    e.preventDefault();
    e.stopPropagation();

    if (this.#isOpen) {
      this.close();
    } else {
      this.open();
    }
  }

  /**
   * Handle keyboard events on trigger
   * @private
   */
  handleTriggerKeyDown(e) {
    const isOpen = this.#isOpen;

    switch (e.key) {
      case 'Enter':
      case ' ':
        e.preventDefault();
        e.stopPropagation();

        if (!isOpen) {
          this.open();
        }
        break;

      case 'ArrowDown':
        e.preventDefault();
        e.stopPropagation();

        if (!isOpen) {
          this.open();
          // Focus first item
          setTimeout(() => {
            this.#keyboardNav?.activateFirst();
          }, 0);
        } else {
          this.#keyboardNav?.activateNext();
        }
        break;

      case 'ArrowUp':
        e.preventDefault();
        e.stopPropagation();

        if (!isOpen) {
          this.open();
          // Focus last item
          setTimeout(() => {
            this.#keyboardNav?.activateLast();
          }, 0);
        } else {
          this.#keyboardNav?.activatePrevious();
        }
        break;

      case 'Escape':
        if (isOpen) {
          e.preventDefault();
          e.stopPropagation();
          this.close();
        }
        break;

      case 'ArrowLeft':
      case 'ArrowRight':
      case 'Tab':
        // Don't intercept these; let default behavior
        break;

      default:
        // Let other keys through
        break;
    }
  }

  /**
   * Handle item click
   * @private
   */
  handleItemClick(e) {
    const item = e.target.closest('[role="option"]');
    if (item && !item.hasAttribute('disabled') && item.getAttribute('aria-disabled') !== 'true') {
      e.preventDefault();
      e.stopPropagation();
      this.selectItem(item);
    }
  }

  /* ─────────────────────────────────────────────────────────────
     OPENING / CLOSING
     ───────────────────────────────────────────────────────────── */

  /**
   * Open the dropdown
   */
  open() {
    if (this.#isOpen || this.#trigger.disabled) return;

    this.#isOpen = true;

    // Show content
    this.#content.classList.add('ren-open');
    this.setAttribute('data-state', 'open');
    this.#trigger.setAttribute('aria-expanded', 'true');

    // Try native popover API
    if ('popover' in HTMLElement.prototype && this.#content.hasAttribute('popover')) {
      try {
        this.#content.showPopover();
      } catch {
        // Fallback to CSS class
      }
    }

    // Attach keyboard nav
    if (this.#keyboardNav) {
      this.#keyboardNav.attach();
      // Activate selected or first item
      const selectedIndex = this.#items.findIndex((item) => {
        const value = item.getAttribute('data-value');
        return this.hasAttribute('multiple')
          ? this.#selectedValue.includes(value)
          : value === this.#selectedValue;
      });
      if (selectedIndex !== -1) {
        this.#keyboardNav.setActiveIndex(selectedIndex);
      } else if (this.#items.length > 0) {
        this.#keyboardNav.activateFirst();
      }
    }

    // Activate dismissable layer
    if (this.#dismissable) {
      this.#dismissable.activate();
    }

    // Position content
    this.positionContent();

    // Reposition on scroll
    this.#scrollController?.abort();
    this.#scrollController = new AbortController();
    window.addEventListener('scroll', this.positionContent, {
      capture: true,
      signal: this.#scrollController.signal,
    });

    // Announce results count for accessibility
    const resultCount = this.#items.length;
    const announcement = document.createElement('div');
    announcement.setAttribute('role', 'status');
    announcement.setAttribute('aria-live', 'polite');
    announcement.className = 'ren-sr-only';
    announcement.textContent = `${resultCount} option${resultCount !== 1 ? 's' : ''} available`;
    this.appendChild(announcement);
    setTimeout(() => announcement.remove(), 1000);

    this.dispatchEvent(new CustomEvent('ren-select-open', { bubbles: true }));
  }

  /**
   * Close the dropdown
   */
  close() {
    if (!this.#isOpen) return;

    this.#isOpen = false;

    // Hide content
    this.#content.classList.remove('ren-open');
    this.setAttribute('data-state', 'closed');
    this.#trigger.setAttribute('aria-expanded', 'false');

    // Try native popover API
    if ('popover' in HTMLElement.prototype && this.#content.hasAttribute('popover')) {
      try {
        this.#content.hidePopover();
      } catch {
        // Fallback
      }
    }

    // Detach keyboard nav
    if (this.#keyboardNav) {
      this.#keyboardNav.detach();
    }

    // Deactivate dismissable layer
    if (this.#dismissable) {
      this.#dismissable.deactivate();
    }

    // Remove scroll listener
    this.#scrollController?.abort();
    this.#scrollController = null;

    // Return focus to trigger
    this.#trigger.focus();

    this.dispatchEvent(new CustomEvent('ren-select-close', { bubbles: true }));
  }

  /**
   * Position dropdown content below or above trigger
   * @private
   */
  positionContent() {
    const placement = normalizeSelectPlacement(this.getAttribute('placement'));
    const { top, left, flipped, side, align } = computePosition(this.#trigger, this.#content, placement);

    this.#content.style.position = 'fixed';
    this.#content.style.top = `${top}px`;
    this.#content.style.left = `${left}px`;
    this.#content.style.width = `${this.#trigger.offsetWidth}px`;
    this.#content.setAttribute('data-flipped', flipped ? 'true' : 'false');
    this.#content.setAttribute('data-side', side);
    this.#content.setAttribute('data-align', align);
    this.setAttribute('data-side', side);
    this.setAttribute('data-align', align);
  }

  /**
   * Mirror preferred placement to public data attributes.
   */
  syncPlacement() {
    const { side, align } = normalizeSelectPlacement(this.getAttribute('placement'));

    this.setAttribute('data-side', side);
    this.setAttribute('data-align', align);
    if (this.#content) {
      this.#content.setAttribute('data-side', side);
      this.#content.setAttribute('data-align', align);
    }
  }

  /* ─────────────────────────────────────────────────────────────
     SELECTION
     ───────────────────────────────────────────────────────────── */

  /**
   * Select an item by element
   * @private
   */
  selectItem(item) {
    const value = item.getAttribute('data-value');
    const label = item.textContent?.trim() || '';

    if (this.hasAttribute('multiple')) {
      const values = [...this.#selectedValue];
      const selectedIndex = values.indexOf(value);
      if (selectedIndex === -1) values.push(value);
      else values.splice(selectedIndex, 1);
      this.selectValue(values, true);
    } else {
      this.selectValue(value, true);
    }

    this.dispatchEvent(
      new CustomEvent('ren-select-change', {
        bubbles: true,
        detail: { value: this.value, label, item },
      })
    );

    // Emit standard change event for forms
    this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));

    // Close dropdown (unless multi-select)
    if (!this.hasAttribute('multiple')) {
      this.close();
    }
  }

  /**
   * Select by value
   * @private
   */
  selectValue(value, updateTrigger = true) {
    if (this.hasAttribute('multiple')) {
      const values = Array.isArray(value) ? value : (value == null ? [] : [value]);
      this.#selectedValue = [...new Set(values.map(String))];
    } else {
      this.#selectedValue = value == null ? null : String(value);
    }

    // Find and update the corresponding item
    const selectedValues = this.hasAttribute('multiple') ? this.#selectedValue : [this.#selectedValue];
    const item = this.#items.find((el) => selectedValues.includes(el.getAttribute('data-value')));
    if (item) {
      this.#selectedItem = item;
    } else {
      this.#selectedItem = null;
    }

    // Update ARIA
    this.updateItemsARIA();

    // Update trigger display
    if (updateTrigger) {
      this.updateTriggerDisplay();
    }

    this.syncHiddenInputs();
  }

  syncHiddenInputs() {
    const name = this.getAttribute('name');
    this.querySelectorAll('input[type="hidden"][data-ren-select-input]').forEach((input) => input.remove());
    this.#hiddenInputs = [];
    if (!name) return;

    const values = this.hasAttribute('multiple')
      ? this.#selectedValue
      : [this.#selectedValue || ''];
    values.forEach((value) => {
      const input = document.createElement('input');
      input.type = 'hidden';
      input.name = name;
      input.value = value;
      input.setAttribute('data-ren-select-input', '');
      this.appendChild(input);
      this.#hiddenInputs.push(input);
    });
  }

  /**
   * Update the trigger button text/icon display
   * @private
   */
  updateTriggerDisplay() {
    const placeholder = this.getAttribute('placeholder') || 'Select an option';

    this.querySelector(':scope > .ren-select-chips[data-ren-select-chips]')?.remove();

    // Clear trigger content
    this.#trigger.innerHTML = '';

    if (this.hasAttribute('multiple') && this.#selectedValue.length > 0) {
      const chips = document.createElement('span');
      chips.className = 'ren-select-chips';
      chips.setAttribute('data-ren-select-chips', '');
      const selectedLabels = [];
      this.#selectedValue.forEach((value) => {
        const item = this.#items.find((option) => option.getAttribute('data-value') === value);
        if (!item) return;

        const chip = document.createElement('span');
        chip.className = 'ren-select-chip';
        const label = document.createElement('span');
        label.textContent = item.textContent?.trim() || value;
        selectedLabels.push(label.textContent);
        const remove = document.createElement('button');
        remove.type = 'button';
        remove.className = 'ren-select-chip-remove';
        remove.dataset.value = value;
        remove.setAttribute('aria-label', `Remove ${label.textContent}`);
        remove.textContent = '×';
        remove.addEventListener('click', (event) => {
          event.preventDefault();
          event.stopPropagation();
          this.selectValue(this.#selectedValue.filter((selected) => selected !== value), true);
          this.dispatchEvent(new CustomEvent('ren-select-change', {
            bubbles: true,
            detail: { value: this.value, label: label.textContent, item },
          }));
          this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
        });
        chip.append(label, remove);
        chips.appendChild(chip);
      });
      this.insertBefore(chips, this.#trigger);

      const valueSpan = document.createElement('span');
      valueSpan.className = 'ren-select-value';
      valueSpan.textContent = selectedLabels.join(', ');
      this.#trigger.appendChild(valueSpan);
    } else if (!this.#selectedValue || !this.#selectedItem) {
      // Show placeholder
      const placeholderSpan = document.createElement('span');
      placeholderSpan.className = 'ren-select-placeholder';
      placeholderSpan.textContent = placeholder;
      this.#trigger.appendChild(placeholderSpan);
    } else {
      // Show selected value
      const valueSpan = document.createElement('span');
      valueSpan.className = 'ren-select-value';
      valueSpan.textContent = this.#selectedItem.textContent?.trim() || '';
      this.#trigger.appendChild(valueSpan);
    }

    // Add chevron icon
    const icon = document.createElement('span');
    icon.className = 'ren-select-icon';
    icon.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>`;
    this.#trigger.appendChild(icon);
  }

  /* ─────────────────────────────────────────────────────────────
     CLEANUP
     ───────────────────────────────────────────────────────────── */

  /**
   * Cleanup event listeners and resources
   * @private
   */
  cleanup() {
    if (this.#keyboardNav) {
      this.#keyboardNav.detach();
    }

    if (this.#dismissable) {
      this.#dismissable.deactivate();
    }

    if (this.#animationFrame) {
      cancelAnimationFrame(this.#animationFrame);
    }

    this.#scrollController?.abort();
    this.#scrollController = null;
    this.#listenerController?.abort();
    this.#listenerController = null;
  }

  /* ─────────────────────────────────────────────────────────────
     PUBLIC API
     ───────────────────────────────────────────────────────────── */

  /**
   * Get current selected value
   * @returns {string|string[]|null} Selected value, ordered values, or null
   */
  get value() {
    return Array.isArray(this.#selectedValue) ? [...this.#selectedValue] : this.#selectedValue;
  }

  /**
   * Set value programmatically
   * @param {string|string[]} value - Value or ordered values to select
   */
  set value(value) {
    this.selectValue(value, true);
  }

  /**
   * Get current selected item element
   * @returns {HTMLElement|null}
   */
  get selectedOption() {
    return this.#selectedItem || null;
  }

  /**
   * Check if dropdown is open
   * @returns {boolean}
   */
  get isOpen() {
    return this.#isOpen;
  }

  /**
   * Get all option items
   * @returns {HTMLElement[]}
   */
  get options() {
    return [...this.#items];
  }

  /**
   * Refresh items from DOM (call after dynamically adding items)
   */
  refresh() {
    this.updateItems();
    this.updateItemsARIA();

    if (this.#keyboardNav) {
      this.#keyboardNav.refresh?.();
    }
  }
}

// Register the custom element
if (!customElements.get('ren-select')) {
  customElements.define('ren-select', RenSelect);
}


id: file:components/composites/ren-sheet/ren-sheet.css
type: css
name: ren-sheet.css
path: components/composites/ren-sheet/ren-sheet.css
data: {}
body:
/* The custom-element host is structural only. Keeping it out of layout prevents
   an empty grid/flex item around the real dialog generated in light DOM. */
ren-sheet {
  display: contents;
}

/* Hide the dialog when it isn't open. The browser default for
   `<dialog>` without `open` is `display: none`, but the rule below
   sets `display: flex` for layout, so without this explicit reset
   the sheet would render in-flow on every page load. */
.ren-sheet:not([open]) {
  display: none;
}

.ren-sheet {
  position: fixed;
  margin: 0;
  padding: 0;
  border: none;

  background: var(--color-surface);
  color: var(--color-text);
  border-radius: 0;

  /* Header/body/footer stack: body grows, footer pins to the bottom */
  display: flex;
  flex-direction: column;

  z-index: 50;
  max-width: 100%;
  max-height: 100%;
  touch-action: none; /* Prevent scroll chaining; swipe-to-dismiss handled by JS */

  &[data-side='right'] {
    inset-inline-end: 0;
    top: 0;
    bottom: 0;

    width: min(24rem, 90vw);
    height: 100dvh;

    @starting-style {
      translate: 100% 0;
    }

    translate: 0 0;
  }

  &[data-side='left'] {
    inset-inline-start: 0;
    top: 0;
    bottom: 0;

    width: min(24rem, 90vw);
    height: 100dvh;

    @starting-style {
      translate: -100% 0;
    }

    translate: 0 0;
  }

  &[data-side='bottom'] {
    bottom: 0;
    inset-inline: 0;

    width: 100vw;
    max-height: 85dvh;

    @starting-style {
      translate: 0 100%;
    }

    translate: 0 0;
  }

  &[data-side='top'] {
    top: 0;
    inset-inline: 0;

    width: 100vw;
    max-height: 85dvh;

    @starting-style {
      translate: 0 -100%;
    }

    translate: 0 0;
  }

  /* Size variants */
  &.-sm {
    width: min(18rem, 90vw);
  }

  &.-md {
    width: min(24rem, 90vw);
  }

  &.-lg {
    width: min(32rem, 90vw);
  }

  &.-xl {
    width: min(42rem, 90vw);
  }

  &.-full {
    width: 100vw;
    height: 100dvh;
  }

  /* Transitions — semantic tokens keep sheet in sync with ren-dialog and
     friends; --duration-enter / --ease-enter collapse to 0ms under
     prefers-reduced-motion via tokens/semantic/motion.css. */
  transition: translate var(--duration-enter) var(--ease-enter),
    opacity var(--duration-enter) var(--ease-enter);
  transition-behavior: allow-discrete;

  /* Reduce motion */
  @media (prefers-reduced-motion: reduce) {
    transition: none;

    @starting-style {
      opacity: 0;
    }

    opacity: 1;
  }

  /* Shadow on appropriate edge */
  &[data-side='right'] {
    box-shadow: -12px 0 32px rgb(0, 0, 0, 0.12);
  }

  &[data-side='left'] {
    box-shadow: 12px 0 32px rgb(0, 0, 0, 0.12);
  }

  &[data-side='bottom'] {
    box-shadow: 0 -12px 32px rgb(0, 0, 0, 0.12);
  }

  &[data-side='top'] {
    box-shadow: 0 12px 32px rgb(0, 0, 0, 0.12);
  }

  /* Backdrop animations — enter uses --duration-enter / --ease-enter,
     close uses the exit pair so the scrim fades at a slightly brisker
     clip than the entry, matching the rest of the overlay family. */
  &[open]::backdrop {
    animation: ren-sheet-backdrop-open var(--duration-enter) var(--ease-enter) forwards;
  }

  &::backdrop {
    @starting-style {
      opacity: 0;
    }

    opacity: 1;
    background: var(--color-overlay);
    backdrop-filter: blur(4px);

    animation: ren-sheet-backdrop-close var(--duration-exit) var(--ease-exit) forwards;
  }

  &:modal::backdrop {
    background: var(--color-overlay);
    backdrop-filter: blur(4px);
  }
}

.ren-sheet-header {
  display: flex;
  align-items: center;
  justify-content: space-between;

  padding: var(--space-6);
  padding-block: var(--space-4);

  border-bottom: 1px solid var(--color-border);
}

.ren-sheet-title {
  font-size: var(--text-lg);
  font-weight: 600;
  color: var(--color-text);
  margin: 0;
}

.ren-sheet-description {
  font-size: var(--text-sm);
  color: var(--color-text-muted);
  margin: var(--space-2) 0 0 0;
}

.ren-sheet-close {
  display: flex;
  align-items: center;
  justify-content: center;

  width: var(--space-8);
  height: var(--space-8);

  padding: 0;
  border: none;
  background: transparent;
  cursor: pointer;

  font-size: 1.5rem;
  color: var(--color-text-muted);

  transition: var(--transition-tactile);

  &:hover {
    color: var(--color-text);
  }

  &:active {
    color: var(--color-text-muted);
  }
}

.ren-sheet-body {
  flex: 1;
  padding: var(--space-6);

  overflow-y: auto;
  overflow-x: hidden;

  /* Scrollbar styling */
  scrollbar-width: thin;
  scrollbar-color: var(--color-fill-active) transparent;

  &::-webkit-scrollbar {
    width: 6px;
  }

  &::-webkit-scrollbar-track {
    background: transparent;
  }

  &::-webkit-scrollbar-thumb {
    background: var(--color-fill-active);
    border-radius: var(--radius-full);
  }

  &::-webkit-scrollbar-thumb:hover {
    background: var(--color-fill-hover);
  }
}

.ren-sheet-footer {
  display: flex;
  gap: var(--space-3);

  padding: var(--space-6);
  padding-block: var(--space-4);

  border-top: 1px solid var(--color-border);
}

.ren-sheet-handle {
  display: flex;
  align-items: center;
  justify-content: center;

  width: 100%;
  height: var(--space-3);

  margin-bottom: var(--space-4);

  &::before {
    content: '';
    display: block;

    width: 40px;
    height: 4px;

    background: var(--color-fill);
    border-radius: var(--radius-full);
  }
}

@keyframes ren-sheet-backdrop-open {
  from {
    opacity: 0;
  }

  to {
    opacity: 1;
  }
}

@keyframes ren-sheet-backdrop-close {
  from {
    opacity: 1;
  }

  to {
    opacity: 0;
  }
}
/* Appearance API bridge */
.ren-sheet {
  width: var(--ren-sheet-width, 320px);
  padding: var(--ren-sheet-padding, var(--space-6));
  border-radius: var(--ren-sheet-radius, 0);
  background: var(--ren-sheet-bg, var(--color-surface-overlay));
  border-color: var(--ren-sheet-border, var(--color-border));
  box-shadow: var(--ren-sheet-shadow, var(--shadow-xl));
  transition-duration: var(--ren-sheet-duration, var(--duration-normal));
  transition-timing-function: var(--ren-sheet-easing, var(--ease-out));
}
.ren-sheet::backdrop { background: var(--ren-sheet-backdrop, var(--color-overlay)); }


