# Casefile, Legal review desk

> A lawyer's desk with an AI assistant: pull contracts with needles, mark up a clause, check the answer's sources, redact, and seal before it leaves. A whole screen for a Next.js app, at /review-desk.

A shadcn/ui block from the [@bevel](https://bevel-ui.vercel.app/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/bevel/review-desk).

- Page, with a live preview: https://shadcn.exchange/r/bevel/review-desk
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/bevel/review-desk-light-226917f22e6a56a0.png
- Registry item: https://bevel-ui.vercel.app/r/review-desk.json
- Categories: blocks
- Listed: 2026-10-20, source last changed 2026-10-20

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/bevel/review-desk.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `https://bevel-ui.vercel.app/r/bevel.json`, `https://bevel-ui.vercel.app/r/approval.json`, `https://bevel-ui.vercel.app/r/citation.json`, `https://bevel-ui.vercel.app/r/keysort-filter.json`, `https://bevel-ui.vercel.app/r/redaction.json`, `https://bevel-ui.vercel.app/r/redline.json`, `https://bevel-ui.vercel.app/r/stamp.json`

## Files

### `registry/bevel/blocks/review-desk/casefile/casefile-app.tsx`

```tsx
"use client"

import * as React from "react"
import { demoMilestone } from "../shared/events"
import { cn } from "@/lib/utils"
import {
  KeysortFilter,
  KeysortFilterCard,
  KeysortFilterCount,
  KeysortFilterDeck,
  KeysortFilterNeedle,
  KeysortFilterNeedles,
} from "@/components/ui/keysort-filter"
import { Redline, RedlineChange, RedlineDocument, RedlineToolbar, type RedlineDecision } from "@/components/ui/redline"
import {
  Citation,
  CitationContent,
  CitationLocation,
  CitationQuote,
  CitationSource,
  CitationStatus,
  CitationTrigger,
} from "@/components/ui/citation"
import { Redaction } from "@/components/ui/redaction"
import {
  Approval,
  ApprovalActions,
  ApprovalDescription,
  ApprovalReject,
  ApprovalSeal,
  ApprovalSignature,
  ApprovalSignatures,
  ApprovalTitle,
  type ApprovalState,
} from "@/components/ui/approval"
import { Stamp, StampDetail } from "@/components/ui/stamp"
import { Legend } from "../shared/environment"
import { documentFace } from "../shared/fonts"
import { changeIds, contracts, needles, type Contract } from "./contracts"

const TODAY = "17 Oct 2026"

type Review = {
  decisions: Record<string, RedlineDecision>
  redacted: string[]
  opened: number[]
  approval: ApprovalState
}

const emptyReview: Review = { decisions: {}, redacted: [], opened: [], approval: "pending" }

function progressOf(contract: Contract, review: Review) {
  const changes = changeIds(contract)
  const decided = changes.filter((id) => review.decisions[id]).length
  const redacted = contract.note.flags.filter((f) => review.redacted.includes(f.id)).length
  const opened = contract.sources.filter((s) => review.opened.includes(s.n)).length
  return {
    changes: { done: decided, total: changes.length },
    redactions: { done: redacted, total: contract.note.flags.length },
    sources: { done: opened, total: contract.sources.length },
    ready: decided === changes.length && redacted === contract.note.flags.length && opened === contract.sources.length,
  }
}

/* The desk. Oak, lit from the upper left, with a grain that runs across it. */
const deskSurface: React.CSSProperties = {
  backgroundColor: "#3A2A1F",
  backgroundImage: [
    "radial-gradient(ellipse 80% 60% at 12% 0%, rgb(255 226 180 / 0.11), transparent 70%)",
    "radial-gradient(ellipse 90% 70% at 100% 100%, rgb(0 0 0 / 0.32), transparent 70%)",
    "repeating-linear-gradient(178deg, rgb(255 255 255 / 0.022) 0 2px, transparent 2px 9px, rgb(0 0 0 / 0.05) 9px 11px, transparent 11px 23px)",
    "repeating-linear-gradient(181deg, transparent 0 41px, rgb(0 0 0 / 0.07) 41px 44px, transparent 44px 97px)",
  ].join(", "),
}

/* Paper lying on the desk: its shadow falls down and to the right, away from the light. */
const onDesk = "shadow-[0_1px_1px_rgb(0_0_0/0.35),6px_14px_28px_-8px_rgb(0_0_0/0.6)]"

/* Words printed on the desk itself. */
const deskLegend = "font-normal italic tracking-normal text-[17px] [text-shadow:0_1px_0_rgb(0_0_0/0.5)]"

/** Casefile: a legal review desk for one matter, with an AI assistant. */
export function CasefileApp() {
  const [pushed, setPushed] = React.useState<string[]>(["signed", "renews"])
  const [openId, setOpenId] = React.useState("msa")
  const [reviews, setReviews] = React.useState<Record<string, Review>>({})

  const contract = contracts.find((c) => c.id === openId) ?? contracts[0]!
  const review = reviews[contract.id] ?? emptyReview
  const update = React.useCallback(
    (id: string, patch: (r: Review) => Partial<Review>) =>
      setReviews((all) => {
        const current = all[id] ?? emptyReview
        return { ...all, [id]: { ...current, ...patch(current) } }
      }),
    []
  )
  const sentCount = contracts.filter((c) => reviews[c.id]?.approval === "approved").length

  const open = (id: string) => {
    setOpenId(id)
    // On a phone the desk is below the drawer: take the reader to it.
    if (window.matchMedia("(max-width: 63.999rem)").matches) {
      document.getElementById("desk")?.scrollIntoView({ behavior: "smooth", block: "start" })
    }
  }

  return (
    <div
      className={cn(
        documentFace.variable,
        "min-h-svh overflow-x-clip font-[family-name:var(--font-document)] text-[#E7DCCB]",
        "[--foreground:#E7DCCB] [--muted-foreground:#B9A992] [--legend:#B9A992] [--ring:#C9B27A]"
      )}
      style={deskSurface}
    >
      <main className="mx-auto max-w-[1360px] px-4 pt-6 pb-20 sm:px-8 lg:pt-10">
        <header className="flex flex-wrap items-end justify-between gap-x-8 gap-y-4">
          <FolderTab />
          <div className="flex items-end gap-8">
            <p className="text-[17px] text-[#B9A992] [text-shadow:0_1px_0_rgb(0_0_0/0.5)]">
              Mira Castell has sent <span className="text-[#E7DCCB] tabular-nums">{sentCount}</span> of {contracts.length}
            </p>
            <BankersLamp />
          </div>
        </header>

        <div className="mt-8 grid gap-x-12 gap-y-12 lg:mt-10 lg:grid-cols-[minmax(0,29rem)_minmax(0,1fr)] [&>*]:min-w-0">
          <Drawer pushed={pushed} onPushedChange={setPushed} openId={contract.id} onOpen={open} reviews={reviews} />

          <div className="flex min-w-0 flex-col gap-12">
            <Desk contract={contract} review={review} update={update} />
            <div className="grid gap-x-10 gap-y-12 md:grid-cols-2 [&>*]:min-w-0">
              <CoverNote contract={contract} review={review} update={update} />
              <Assistant contract={contract} review={review} update={update} />
            </div>
            <Outbox contract={contract} review={review} update={update} />
          </div>
        </div>
      </main>
    </div>
  )
}

/** The banker's lamp, seen from above: green glass shade on a brass arm. Its light falls on the open contract. */
function BankersLamp() {
  return (
    <div aria-hidden className="relative hidden h-24 w-56 lg:block">
      {/* Brass base and stem. */}
      <span className="absolute top-1 left-1/2 size-14 -translate-x-1/2 rounded-full bg-[radial-gradient(circle_at_35%_30%,#E9CF8A,#9C7A35_60%,#5E4718)] shadow-[4px_10px_18px_-4px_rgb(0_0_0/0.7)]" />
      {/* The shade: a long half-cylinder of cased green glass. */}
      <span className="absolute top-9 left-0 h-14 w-56 rounded-[28px] bg-[linear-gradient(180deg,#5FA571_0%,#1F6237_38%,#0E3A20_100%)] shadow-[inset_0_2px_0_rgb(255_255_255/0.35),inset_0_-3px_6px_rgb(0_0_0/0.5),6px_16px_24px_-6px_rgb(0_0_0/0.75)]" />
      <span className="absolute top-[42px] left-6 h-1.5 w-44 rounded-full bg-white/30 blur-[2px]" />
      {/* Brass end caps. */}
      <span className="absolute top-9 left-0 h-14 w-3 rounded-l-[28px] bg-[linear-gradient(180deg,#E3C67E,#7D5F24)]" />
      <span className="absolute top-9 right-0 h-14 w-3 rounded-r-[28px] bg-[linear-gradient(180deg,#E3C67E,#7D5F24)]" />
    </div>
  )
}

/** The matter's file folder, its typed label on the tab. */
function FolderTab() {
  return (
    <div className="relative min-w-0 max-w-full">
      {/* The tab, rising from the folder's edge. */}
      <div className={cn("relative z-10 inline-block max-w-full rounded-t-[10px] bg-[#D8BE8A] px-3 pt-3 pb-2", "shadow-[inset_0_1px_0_rgb(255_255_255/0.35)]")}>
        <div className="rounded-[2px] bg-[#FBF7EE] px-4 py-2 shadow-[0_1px_1px_rgb(0_0_0/0.25)]">
          <h1 className="text-[22px] leading-tight font-semibold text-[#2B2620] sm:text-[26px]">
            Northwind v. Halberd, supply dispute
          </h1>
          <p className="mt-0.5 text-[15px] text-[#6B6152]">Matter 2026-0417, commercial litigation</p>
        </div>
      </div>
      {/* The folder's front edge, running on under the papers. */}
      <div aria-hidden className={cn("-mt-px h-7 w-[calc(100%+4rem)] max-w-[calc(100vw-2rem)] rounded-r-[6px] bg-[#CFB37C]", onDesk)} />
    </div>
  )
}

function Drawer({
  pushed,
  onPushedChange,
  openId,
  onOpen,
  reviews,
}: {
  pushed: string[]
  onPushedChange: (value: string[]) => void
  openId: string
  onOpen: (id: string) => void
  reviews: Record<string, Review>
}) {
  return (
    <section id="drawer" aria-labelledby="drawer-title" className="flex scroll-mt-4 flex-col gap-3 lg:sticky lg:top-6 lg:self-start">
      <Legend id="drawer-title" className={deskLegend}>
        Drawer
      </Legend>
      {/* A card file box: walnut sides, the cards standing in its well. */}
      <div className="rounded-[14px] bg-[#4A3526] p-1.5 sm:p-2.5 shadow-[inset_0_1px_0_rgb(255_255_255/0.12),0_2px_2px_rgb(0_0_0/0.4),8px_18px_36px_-10px_rgb(0_0_0/0.7)]">
        <div className="rounded-[8px] bg-[#241911] px-1.5 pt-4 pb-4 [--keysort-filter-column:2.3rem] sm:[--keysort-filter-column:3.5rem] shadow-[inset_0_6px_14px_rgb(0_0_0/0.65),inset_0_-1px_0_rgb(255_255_255/0.06)] sm:px-4 lg:[--keysort-filter-column:3.4rem]">
          <KeysortFilter aria-label="Contracts in this matter" value={pushed} onValueChange={onPushedChange} className="text-[#E7DCCB]">
            <KeysortFilterNeedles className="[&_[data-slot=keysort-filter-needle]]:text-[12px] sm:[&_[data-slot=keysort-filter-needle]]:text-[13px]">
              {needles.map((n) => (
                <KeysortFilterNeedle key={n.value} value={n.value}>
                  {n.label}
                </KeysortFilterNeedle>
              ))}
            </KeysortFilterNeedles>
            <KeysortFilterDeck>
              {contracts.map((c) => {
                const isOpen = c.id === openId
                const state = reviews[c.id]?.approval
                return (
                  <KeysortFilterCard
                    key={c.id}
                    notches={c.tags}
                    data-open={isOpen ? "" : undefined}
                    className="data-open:ring-2 data-open:ring-[#7FB08A] data-open:ring-offset-2 data-open:ring-offset-[#241911]"
                  >
                    <button
                      type="button"
                      onClick={() => onOpen(c.id)}
                      aria-current={isOpen ? "true" : undefined}
                      className="cursor-pointer rounded-sm text-left text-[14px] outline-none sm:text-[15px] hover:underline focus-visible:ring-2 focus-visible:ring-[#7FB08A]"
                    >
                      {c.title}
                      {state && (
                        <span className="block text-[13px] font-normal italic opacity-80">
                          {state === "approved" ? "Sent" : "Sent back"}
                        </span>
                      )}
                    </button>
                  </KeysortFilterCard>
                )
              })}
            </KeysortFilterDeck>
            <KeysortFilterCount className="px-1 text-[15px] text-[#B9A992]" />
          </KeysortFilter>
        </div>
      </div>
    </section>
  )
}

type PanelProps = {
  contract: Contract
  review: Review
  update: (id: string, patch: (r: Review) => Partial<Review>) => void
}

function SentStamp({ className }: { className?: string }) {
  return (
    <Stamp tone="blue" animate className={cn("pointer-events-none absolute", className)}>
      Sent
      <StampDetail>{TODAY}</StampDetail>
    </Stamp>
  )
}

/** The hero: the open contract, under the lamp. */
function Desk({ contract, review, update }: PanelProps) {
  const sent = review.approval === "approved"
  return (
    <section id="desk" aria-labelledby="desk-title" className="relative flex scroll-mt-4 flex-col gap-3">
      <Legend id="desk-title" className={deskLegend}>
        On the desk
      </Legend>
      {/* The banker's lamp: a warm pool on the page, a little green from its shade. */}
      <div
        aria-hidden
        className="pointer-events-none absolute -inset-x-16 -top-20 -bottom-24"
        style={{
          background:
            "radial-gradient(ellipse 58% 52% at 50% 45%, rgb(255 238 200 / 0.34), rgb(170 215 160 / 0.12) 50%, transparent 74%)",
        }}
      />
      <div className="relative z-10 [--bevel-paper:oklch(0.965_0.014_85)] sm:rotate-[-0.6deg]">
        <Redline
          key={contract.id}
          className={cn("max-w-none", onDesk)}
          decisions={review.decisions}
          onDecisionsChange={(decisions) => update(contract.id, () => ({ decisions }))}
        >
          <div className="flex flex-wrap items-start gap-x-4 gap-y-2 px-6 pt-6 sm:px-10 sm:pt-9">
            <div className="min-w-0 flex-1">
              <p className="text-[24px] leading-tight font-semibold text-(--bevel-paper-foreground) sm:text-[28px]">
                {contract.title}
              </p>
              <p className="mt-1 text-[15px] text-(--bevel-paper-muted)">{contract.parties}</p>
            </div>
            <Stamp tone="red" className="text-[15px]">
              Privileged
            </Stamp>
          </div>
          <RedlineDocument className="px-6 pt-5 pb-7 text-[18px] leading-[2.1] sm:px-10 sm:text-[21px]">
            <strong>
              {contract.clause.number} {contract.clause.heading}.
            </strong>{" "}
            {contract.clause.body.map((s, i) =>
              typeof s === "string" ? (
                <React.Fragment key={i}>{s}</React.Fragment>
              ) : (
                <RedlineChange key={s.change} id={s.change} deletion={s.deletion} insertion={s.insertion} />
              )
            )}
          </RedlineDocument>
          {sent ? (
            <p className="border-t border-(--bevel-paper-rule) px-6 py-3 text-[15px] text-(--bevel-paper-muted) sm:px-10">
              Sent to {contract.note.to}. The clause is locked.
            </p>
          ) : (
            <RedlineToolbar className="flex-wrap px-6 text-[15px] sm:px-10" />
          )}
        </Redline>
        {sent && <SentStamp className="right-8 bottom-16" />}
      </div>
    </section>
  )
}

/** The cover note, tucked under the contract, with the assistant's flags on a sticky note. */
function CoverNote({ contract, review, update }: PanelProps) {
  const progress = progressOf(contract, review)
  const { note } = contract
  const sent = review.approval === "approved"
  const locked = review.approval !== "pending"
  const flag = (id: string) => note.flags.find((f) => f.id === id)!

  return (
    <section aria-label="Cover note" className="relative md:-mt-16 md:ml-2">
      <article
        aria-label="Cover note, as the recipient will see it"
        className={cn(
          "bevel-paper relative flex flex-col gap-3 rounded-(--bevel-radius-xs) px-6 pt-6 pb-24 text-[15px] leading-relaxed md:rotate-[1.3deg] md:pt-20",
          onDesk
        )}
      >
        <dl className="grid grid-cols-[auto_minmax(0,1fr)] gap-x-3 gap-y-0.5 border-b border-(--bevel-paper-rule) pb-3 text-[14px] text-(--bevel-paper-muted)">
          <dt>To</dt>
          <dd className="break-words">
            {note.to}, {note.firm}
          </dd>
          <dt>Re</dt>
          <dd>{note.subject}</dd>
          <dt>Encl.</dt>
          <dd>
            Clause {contract.clause.number}, {progress.changes.done} of {progress.changes.total} edits decided
          </dd>
        </dl>
        {note.body.map((paragraph, i) => (
          <p key={i}>
            {paragraph.map((seg, j) => {
              if (typeof seg === "string") return <React.Fragment key={j}>{seg}</React.Fragment>
              const f = flag(seg.redact)
              return review.redacted.includes(f.id) ? (
                <Redaction key={j} length={Math.min(f.text.length, 36)} reason={f.reason} />
              ) : (
                <mark key={j} className="rounded-sm bg-(--bevel-pen-red)/12 px-0.5 text-inherit box-decoration-clone">
                  <span className="sr-only">Flagged as {f.reason}: </span>
                  {f.text}
                </mark>
              )
            })}
          </p>
        ))}
        <p>
          Kind regards,
          <br />
          Castell Rowe LLP
        </p>
        {sent && <SentStamp className="top-24 right-6" />}
      </article>

      {/* The assistant's flags, on a sticky note stuck to the bottom of the page. */}
      <fieldset
        disabled={locked}
        className={cn(
          "relative z-10 mx-auto -mt-20 flex [color-scheme:light] w-[min(100%,19rem)] flex-col gap-2 rounded-[2px] bg-[#F2DE7C] px-4 pt-3 pb-4 text-[15px] text-[#3B3215] rotate-[-2deg] sm:mr-4",
          "shadow-[0_1px_1px_rgb(0_0_0/0.25),4px_10px_16px_-6px_rgb(0_0_0/0.5)]"
        )}
      >
        <legend className="float-left mb-1 w-full font-semibold">Redact before sending</legend>
        {note.flags.map((f) => (
          <label key={f.id} className="flex cursor-pointer items-start gap-2 leading-snug has-disabled:cursor-default">
            <input
              type="checkbox"
              className="mt-1 size-4 shrink-0 accent-[#3B3215]"
              checked={review.redacted.includes(f.id)}
              onChange={(e) =>
                update(contract.id, (r) => ({
                  redacted: e.target.checked ? [...r.redacted, f.id] : r.redacted.filter((x) => x !== f.id),
                }))
              }
            />
            <span>
              {f.label} <span className="italic opacity-75">({f.reason})</span>
            </span>
          </label>
        ))}
      </fieldset>
    </section>
  )
}

/** The assistant's answer, printed as a memo, every claim stickered to its source. */
function Assistant({ contract, review, update }: PanelProps) {
  return (
    <section id="assistant" aria-labelledby="assistant-title" className="flex scroll-mt-4 flex-col gap-3">
      <Legend id="assistant-title" className={deskLegend}>
        Assistant
      </Legend>
      <article
        className={cn(
          "bevel-paper flex flex-col gap-4 rounded-(--bevel-radius-xs) px-6 py-6 text-[16px] leading-relaxed md:rotate-[-1.4deg]",
          onDesk
        )}
      >
        <p className="border-b border-(--bevel-paper-rule) pb-3 text-[17px] italic text-(--bevel-paper-foreground)">
          <span className="sr-only">You asked: </span>
          {contract.question}
        </p>
        {contract.answer.map((paragraph, i) => (
          <p key={i}>
            {paragraph.map((seg, j) => {
              if (typeof seg === "string") return <React.Fragment key={j}>{seg}</React.Fragment>
              const source = contract.sources.find((s) => s.n === seg.cite)!
              return (
                <Citation
                  key={j}
                  onOpenChange={(isOpen) => {
                    if (isOpen) update(contract.id, (r) => ({ opened: r.opened.includes(source.n) ? r.opened : [...r.opened, source.n] }))
                  }}
                >
                  <CitationTrigger tone={source.external ? "blue" : "yellow"} abbreviation="Src." label="Source">
                    {source.n}
                  </CitationTrigger>
                  <CitationContent>
                    <CitationSource>{source.source}</CitationSource>
                    <CitationLocation>{source.location}</CitationLocation>
                    <CitationQuote>{source.quote}</CitationQuote>
                    <CitationStatus status={source.status}>{source.statusText}</CitationStatus>
                  </CitationContent>
                </Citation>
              )
            })}
          </p>
        ))}
        <ol aria-label="Sources" className="flex flex-col gap-1.5 border-t border-(--bevel-paper-rule) pt-3 text-[14px] leading-snug text-(--bevel-paper-muted)">
          {contract.sources.map((s) => {
            const opened = review.opened.includes(s.n)
            return (
              <li key={s.n} className="flex gap-2">
                <span className="tabular-nums">{s.n}.</span>
                <span className="min-w-0 flex-1">
                  {s.source}, {s.location}
                </span>
                <span className={cn("shrink-0 italic", opened && "text-(--bevel-pen-blue)")}>
                  {opened ? (s.status === "unverified" ? "Not found" : "Checked") : "Unread"}
                </span>
              </li>
            )
          })}
        </ol>
      </article>
    </section>
  )
}

function Check({ done, total, label }: { done: number; total: number; label: string }) {
  const complete = done === total
  return (
    <li className="flex items-start gap-2.5">
      <span
        aria-hidden
        className="mt-0.5 grid size-5 shrink-0 place-items-center rounded-[3px] border border-(--bevel-paper-foreground)/45 text-[17px] leading-none font-bold text-(--bevel-pen-blue)"
      >
        {complete ? "✓" : ""}
      </span>
      <span className="min-w-0 flex-1">
        {label}
        <span className="sr-only">{complete ? ", done" : `, ${done} of ${total}`}</span>
      </span>
      {!complete && (
        <span aria-hidden className="tabular-nums text-(--bevel-paper-muted)">
          {done}/{total}
        </span>
      )}
    </li>
  )
}

/** Before it leaves: the checklist slip and the seal press. */
function Outbox({ contract, review, update }: PanelProps) {
  const progress = progressOf(contract, review)
  const { note } = contract
  const sent = review.approval === "approved"
  const locked = review.approval !== "pending"

  return (
    <section id="outbox" aria-labelledby="outbox-title" className="flex scroll-mt-4 flex-col gap-3">
      <Legend id="outbox-title" className={deskLegend}>
        Before it leaves
      </Legend>
      <div className="flex flex-col items-stretch gap-8 sm:flex-row sm:items-start sm:justify-end">
        <div
          className={cn(
            "bevel-paper relative z-10 flex flex-col gap-2 self-center rounded-[2px] px-5 py-4 text-[15px] leading-snug sm:mt-14 sm:-mr-3 sm:w-64 sm:shrink-0 sm:rotate-[2.6deg]",
            onDesk
          )}
        >
          <ul aria-label="Checklist" className="flex flex-col gap-2.5">
            <Check {...progress.changes} label="Decide every edit" />
            <Check {...progress.sources} label="Read every source" />
            <Check {...progress.redactions} label="Redact every flag" />
          </ul>
        </div>

        <Approval
          key={contract.id}
          className={cn("max-w-none sm:max-w-md sm:rotate-[-0.8deg] text-[15px]", onDesk)}
          state={review.approval}
          onApprove={() => {
            if (!progress.ready) return
            update(contract.id, () => ({ approval: "approved" }))
            demoMilestone("casefile", "seal")
          }}
          onReject={() => update(contract.id, () => ({ approval: "rejected" }))}
        >
          <ApprovalTitle className="text-[19px]">Send to {note.firm}</ApprovalTitle>
          <ApprovalDescription>
            The redacted note and clause {contract.clause.number} go to {note.to}. Sent email can&apos;t be recalled.
          </ApprovalDescription>
          <ApprovalSignatures>
            <ApprovalSignature name="Dana Okafor" signed />
            <ApprovalSignature name="Mira Castell" signed={sent} />
          </ApprovalSignatures>
          {!locked && !progress.ready && (
            <p className="text-(--bevel-pen-red)">The seal is locked until the checklist is done.</p>
          )}
          <ApprovalActions className="flex-wrap">
            <ApprovalReject>Send back</ApprovalReject>
            <ApprovalSeal
              disabled={!progress.ready}
              className="size-20 disabled:cursor-not-allowed disabled:opacity-50 data-disabled:cursor-not-allowed data-disabled:opacity-50"
            />
          </ApprovalActions>
          {locked && (
            <button
              type="button"
              onClick={() => update(contract.id, () => ({ approval: "pending" }))}
              className="h-9 self-start rounded-(--bevel-radius-sm) border border-(--bevel-paper-foreground)/30 px-3 font-medium text-(--bevel-paper-foreground) outline-none hover:bg-(--bevel-paper-foreground)/5 focus-visible:ring-2 focus-visible:ring-(--bevel-pen-blue)/50"
            >
              {sent ? "Reset this contract (demo)" : "Reopen for review"}
            </button>
          )}
        </Approval>
      </div>
    </section>
  )
}
```

### `registry/bevel/blocks/review-desk/casefile/contracts.tsx`

```tsx
/**
 * The contracts in the Northwind v. Halberd drawer. Everything here is
 * fictional: parties, people, numbers, cases.
 */

/** A run of clause text, or one of the assistant's proposed edits. */
export type ClauseSegment = string | { change: string; deletion?: string; insertion?: string }

/** A run of answer text, or a pinned source by number. */
export type AnswerSegment = string | { cite: number }

/** A run of the cover note, or a detail the assistant flagged for redaction. */
export type NoteSegment = string | { redact: string }

export type Source = {
  n: number
  source: string
  location: string
  quote: string
  status: "verified" | "caution" | "unverified"
  statusText?: string
  /** A non-contract source gets a blue sticker. */
  external?: boolean
}

export type Flag = { id: string; label: string; reason: string; text: string }

export type Contract = {
  id: string
  title: string
  parties: string
  ref: string
  tags: string[]
  clause: { number: string; heading: string; body: ClauseSegment[] }
  question: string
  answer: AnswerSegment[][]
  sources: Source[]
  note: {
    to: string
    firm: string
    email: string
    subject: string
    body: NoteSegment[][]
    flags: Flag[]
  }
}

export const needles = [
  { value: "nda", label: "NDA" },
  { value: "signed", label: "Signed" },
  { value: "eu", label: "EU law" },
  { value: "renews", label: "Renews" },
  { value: "dpa", label: "DPA" },
] as const

export const contracts: Contract[] = [
  {
    id: "msa",
    title: "Master supply agreement",
    parties: "Northwind Freight B.V. and Halberd Components GmbH",
    ref: "NW-031",
    tags: ["signed", "eu", "renews"],
    clause: {
      number: "14.3",
      heading: "Limitation of liability",
      body: [
        "Neither party's total liability under this Agreement shall exceed the fees paid in the ",
        { change: "cap", deletion: "three (3)", insertion: "twelve (12)" },
        " months before the claim arose. This cap does not apply to ",
        { change: "carve", insertion: "breach of clause 9 (Confidentiality), " },
        "fraud or wilful misconduct",
        { change: "notice", insertion: ", and no claim is barred for want of notice given within ninety (90) days" },
        ".",
      ],
    },
    question: "Can Northwind recover the full cost of the March shortfall, or does the cap bite?",
    answer: [
      [
        "Probably not in full. Liability is capped at three months' fees",
        { cite: 1 },
        ", about €410,000 on the current run rate, against a claimed loss of €1.2m.",
      ],
      [
        "The cap doesn't apply to wilful misconduct, and Halberd's own email shows it moved Northwind's allocated stock to another buyer",
        { cite: 2 },
        ". The agreement renewed on 1 January",
        { cite: 3 },
        ", so the current terms apply. I found a case striking down a similar cap",
        { cite: 4 },
        ", but couldn't confirm it exists. Don't rely on it.",
      ],
    ],
    sources: [
      {
        n: 1,
        source: "Master supply agreement",
        location: "p. 22, clause 14.3",
        quote: "Neither party's total liability under this Agreement shall exceed the fees paid in the three (3) months before the claim arose.",
        status: "verified",
      },
      {
        n: 2,
        source: "Halberd internal email, 4 Mar 2026",
        location: "Disclosure bundle, tab 7",
        quote: "Move the Northwind allocation to Vantor for this run. We'll make it up in April.",
        status: "verified",
        external: true,
      },
      {
        n: 3,
        source: "Master supply agreement",
        location: "p. 3, clause 2.2",
        quote: "This Agreement renews automatically for successive twelve-month terms unless either party gives ninety (90) days' notice.",
        status: "verified",
      },
      {
        n: 4,
        source: "Brennan Freight v. Ostmark AG (2021)",
        location: "¶ 18, as cited",
        quote: "No matching passage found in any reporter Casefile can search.",
        status: "unverified",
        external: true,
      },
    ],
    note: {
      to: "Ines Varga",
      firm: "Varga Lindqvist",
      email: "i.varga@vargalindqvist.example",
      subject: "Northwind / Halberd: proposed amendment to clause 14.3",
      body: [
        ["Dear Ms Varga,"],
        [
          "Further to our call, please find our proposed amendment to clause 14.3 of the supply agreement. ",
          { redact: "contact" },
          " can confirm the shortfall figures on request.",
        ],
        [{ redact: "strategy" }, " We look forward to your client's response by 31 October."],
      ],
      flags: [
        { id: "contact", label: "Client contact's name and mobile", reason: "personal data", text: "Our client's operations lead, Tomas Reyes (+31 6 1234 5678)," },
        { id: "strategy", label: "Our view on the cap", reason: "privileged", text: "We think the cap is unlikely to survive the diversion of stock." },
      ],
    },
  },
  {
    id: "nda",
    title: "Mutual NDA",
    parties: "Northwind Freight B.V. and Halberd Components GmbH",
    ref: "NW-012",
    tags: ["nda", "signed"],
    clause: {
      number: "5.1",
      heading: "Duration of confidentiality",
      body: [
        "The obligations in this Agreement last for ",
        { change: "term", deletion: "two (2)", insertion: "five (5)" },
        " years after each disclosure",
        { change: "secrets", insertion: ", and for trade secrets, for as long as the information remains a trade secret" },
        ".",
      ],
    },
    question: "Can we put Halberd's pricing sheet in front of the court?",
    answer: [
      [
        "Yes, with care. Either party may disclose Confidential Information when a court requires it",
        { cite: 1 },
        ", provided it gives the other side prompt written notice first",
        { cite: 2 },
        ".",
      ],
      [
        "The sheet is marked confidential but undated",
        { cite: 3 },
        ", so we should file it under seal and confirm which version Halberd sent.",
      ],
    ],
    sources: [
      {
        n: 1,
        source: "Mutual NDA",
        location: "p. 4, clause 6.2",
        quote: "Nothing in this Agreement prevents disclosure required by law or by order of a competent court.",
        status: "verified",
      },
      {
        n: 2,
        source: "Mutual NDA",
        location: "p. 4, clause 6.3",
        quote: "The disclosing party shall give prompt written notice before any such disclosure, where lawful.",
        status: "verified",
      },
      {
        n: 3,
        source: "Halberd pricing sheet",
        location: "Disclosure bundle, tab 12, footer",
        quote: "Halberd Components. Confidential. Not for distribution.",
        status: "caution",
        statusText: "Undated",
        external: true,
      },
    ],
    note: {
      to: "Ines Varga",
      firm: "Varga Lindqvist",
      email: "i.varga@vargalindqvist.example",
      subject: "Northwind / Halberd: notice under clause 6.3 of the NDA",
      body: [
        ["Dear Ms Varga,"],
        [
          "We give notice under clause 6.3 that our client intends to file Halberd's pricing sheet with the court, under seal. We also attach a proposed amendment to clause 5.1.",
        ],
        [{ redact: "author" }],
      ],
      flags: [
        { id: "author", label: "Associate's direct line", reason: "personal data", text: "Drafted by Mira Castell, direct line +44 20 7946 0113." },
      ],
    },
  },
  {
    id: "dpa",
    title: "Data processing addendum",
    parties: "Northwind Freight B.V. (controller) and Halberd Components GmbH (processor)",
    ref: "NW-033",
    tags: ["signed", "eu", "dpa"],
    clause: {
      number: "8.1",
      heading: "Breach notification",
      body: [
        "The processor shall notify the controller of a personal data breach ",
        { change: "speed", deletion: "without undue delay", insertion: "within twenty-four (24) hours" },
        " after becoming aware of it",
        { change: "annex", insertion: ", with the information listed in Annex 3" },
        ".",
      ],
    },
    question: "Did Halberd breach the DPA when the shipment manifests leaked?",
    answer: [
      [
        "Arguably. Halberd had to tell us without undue delay",
        { cite: 1 },
        ", and its first notice came nine days after it found the leak",
        { cite: 2 },
        ".",
      ],
      [
        "We then had 72 hours to tell the regulator",
        { cite: 3 },
        ", which a nine-day gap makes hard. That's why I suggest a fixed 24-hour window.",
      ],
    ],
    sources: [
      {
        n: 1,
        source: "Data processing addendum",
        location: "p. 6, clause 8.1",
        quote: "The processor shall notify the controller of a personal data breach without undue delay after becoming aware of it.",
        status: "verified",
      },
      {
        n: 2,
        source: "Halberd letter, 22 Aug 2026",
        location: "Disclosure bundle, tab 19, para. 2",
        quote: "Our team identified the exposure on 13 August and has since contained it.",
        status: "verified",
        external: true,
      },
      {
        n: 3,
        source: "GDPR, Article 33(1)",
        location: "Regulation (EU) 2016/679",
        quote: "…not later than 72 hours after having become aware of it…",
        status: "verified",
        external: true,
      },
    ],
    note: {
      to: "Ines Varga",
      firm: "Varga Lindqvist",
      email: "i.varga@vargalindqvist.example",
      subject: "Northwind / Halberd: breach notification under the DPA",
      body: [
        ["Dear Ms Varga,"],
        [
          "The leaked manifests included the home address of ",
          { redact: "driver" },
          " Please confirm what was exposed and when, and see our proposed amendment to clause 8.1.",
        ],
        [{ redact: "regulator" }],
      ],
      flags: [
        { id: "driver", label: "Driver's name", reason: "personal data", text: "one of our client's drivers, Pieter Aalders." },
        { id: "regulator", label: "Internal note on the regulator", reason: "privileged", text: "Internal: we reported late to the regulator and may face a fine." },
      ],
    },
  },
  {
    id: "whs",
    title: "Kessler warehousing",
    parties: "Northwind Freight B.V. and Kessler Logistics Ltd",
    ref: "NW-020",
    tags: ["signed", "renews"],
    clause: {
      number: "11.2",
      heading: "Termination for supply failure",
      body: [
        "Northwind may end this Agreement on ",
        { change: "notice", deletion: "six (6) months'", insertion: "sixty (60) days'" },
        " written notice if its principal supplier fails to deliver for ",
        { change: "weeks", deletion: "twelve (12)", insertion: "eight (8)" },
        " consecutive weeks.",
      ],
    },
    question: "If Halberd stock stops, can Northwind get out of the Kessler warehouse?",
    answer: [
      [
        "Yes, but slowly. Northwind can end it on six months' notice once supply has failed for twelve weeks",
        { cite: 1 },
        ". Until then it pays for the space, empty or not",
        { cite: 2 },
        ".",
      ],
    ],
    sources: [
      {
        n: 1,
        source: "Kessler warehousing agreement",
        location: "p. 9, clause 11.2",
        quote: "Northwind may end this Agreement on six (6) months' written notice if its principal supplier fails to deliver for twelve (12) consecutive weeks.",
        status: "verified",
      },
      {
        n: 2,
        source: "Kessler warehousing agreement",
        location: "Schedule 1, fees",
        quote: "Storage fees are payable monthly for the reserved area, whether or not used.",
        status: "verified",
      },
    ],
    note: {
      to: "Owen Hartley",
      firm: "Kessler Logistics",
      email: "legal@kesslerlogistics.example",
      subject: "Proposed amendment to clause 11.2",
      body: [
        ["Dear Mr Hartley,"],
        [
          "Given the supply position, our client proposes the attached amendment to clause 11.2. ",
          { redact: "dispute" },
        ],
      ],
      flags: [
        { id: "dispute", label: "Reference to the Halberd claim", reason: "privileged", text: "This is driven by our claim against Halberd, which we expect to settle." },
      ],
    },
  },
  {
    id: "ts",
    title: "Settlement term sheet, draft",
    parties: "Northwind Freight B.V. and Halberd Components GmbH",
    ref: "NW-041",
    tags: ["eu"],
    clause: {
      number: "3",
      heading: "Payment",
      body: [
        "Halberd shall pay Northwind ",
        { change: "sum", deletion: "€600,000", insertion: "€850,000" },
        " in full and final settlement, within ",
        { change: "days", deletion: "sixty (60)", insertion: "thirty (30)" },
        " days of signature.",
      ],
    },
    question: "Is €850,000 a defensible opening number?",
    answer: [
      [
        "It's within range. The cap limits a contract claim to about €410,000",
        { cite: 1 },
        ", but the misconduct carve-out could lift it to the full €1.2m loss",
        { cite: 2 },
        ". €850,000 prices in roughly even odds on the carve-out.",
      ],
    ],
    sources: [
      {
        n: 1,
        source: "Master supply agreement",
        location: "p. 22, clause 14.3",
        quote: "…shall exceed the fees paid in the three (3) months before the claim arose.",
        status: "verified",
      },
      {
        n: 2,
        source: "Loss schedule, v4",
        location: "Expert report, table 2",
        quote: "Total loss, March shortfall: €1,204,300.",
        status: "caution",
        statusText: "Draft",
        external: true,
      },
    ],
    note: {
      to: "Ines Varga",
      firm: "Varga Lindqvist",
      email: "i.varga@vargalindqvist.example",
      subject: "Without prejudice: settlement terms",
      body: [
        ["Dear Ms Varga,"],
        [
          "Without prejudice save as to costs, we enclose revised settlement terms. ",
          { redact: "floor" },
        ],
      ],
      flags: [
        { id: "floor", label: "Client's walk-away figure", reason: "privileged", text: "Our client would accept €700,000 if pressed." },
      ],
    },
  },
  {
    id: "orrin",
    title: "Orrin Metals NDA",
    parties: "Northwind Freight B.V. and Orrin Metals S.A.",
    ref: "NW-044",
    tags: ["nda", "renews"],
    clause: {
      number: "2.1",
      heading: "Permitted purpose",
      body: [
        "Northwind may use the Information only to evaluate ",
        { change: "purpose", deletion: "a supply relationship", insertion: "a replacement supply relationship" },
        " with Orrin",
        { change: "advisers", insertion: ", and may share it with its legal advisers" },
        ".",
      ],
    },
    question: "Can we show Orrin's quote to our expert as evidence of replacement cost?",
    answer: [
      [
        "Not yet. Use is limited to evaluating a supply relationship",
        { cite: 1 },
        ", and only named recipients may see it",
        { cite: 2 },
        ". The proposed edit adds legal advisers, which would cover the expert through us.",
      ],
    ],
    sources: [
      {
        n: 1,
        source: "Orrin Metals NDA",
        location: "p. 1, clause 2.1",
        quote: "Northwind may use the Information only to evaluate a supply relationship with Orrin.",
        status: "verified",
      },
      {
        n: 2,
        source: "Orrin Metals NDA",
        location: "p. 2, clause 3.4",
        quote: "Information may be shared only with the employees named in Schedule A.",
        status: "verified",
      },
    ],
    note: {
      to: "Lucía Ferrer",
      firm: "Orrin Metals",
      email: "l.ferrer@orrinmetals.example",
      subject: "NDA: request to extend permitted purpose",
      body: [
        ["Dear Ms Ferrer,"],
        [
          "We act for Northwind and propose the attached change to clause 2.1, so we can review your quote with our client. ",
          { redact: "reason" },
        ],
      ],
      flags: [
        { id: "reason", label: "Why we really want it", reason: "privileged", text: "We intend to use it as evidence of replacement cost against Halberd." },
      ],
    },
  },
]

/** Every change id in a clause. */
export function changeIds(contract: Contract) {
  return contract.clause.body.flatMap((s) => (typeof s === "string" ? [] : [s.change]))
}
```

### `registry/bevel/blocks/review-desk/shared/environment.tsx`

```tsx
import * as React from "react"
import { cn } from "@/lib/utils"

/**
 * Pieces of the physical environments the demos are set in. Each demo
 * mounts Bevel components on a surface; these are the surface's own parts.
 */

/** A legend engraved into the surface: names the control below it. */
export function Legend({ className, ...props }: React.ComponentProps<"p">) {
  return (
    <p
      className={cn(
        "text-[15px] leading-none font-semibold tracking-wide text-(--legend) [text-shadow:0_-1px_0_rgb(0_0_0/0.45),0_1px_0_rgb(255_255_255/0.08)]",
        className
      )}
      {...props}
    />
  )
}

/** A slotted screw head, for panel corners. */
export function Screw({ className, angle = 30 }: { className?: string; angle?: number }) {
  return (
    <span
      aria-hidden
      className={cn("bevel-metal absolute size-3.5 rounded-full shadow-(--bevel-depth-raised)", className)}
    >
      <span className="absolute inset-x-0.5 top-1/2 h-px -translate-y-1/2 bg-black/50" style={{ rotate: `${angle}deg` }} />
    </span>
  )
}

/** Four screws, one in each corner of a positioned panel. */
const screwCorners = {
  "2": ["top-2 left-2", "top-2 right-2", "bottom-2 left-2", "bottom-2 right-2"],
  "3": ["top-3 left-3", "top-3 right-3", "bottom-3 left-3", "bottom-3 right-3"],
  "4": ["top-4 left-4", "top-4 right-4", "bottom-4 left-4", "bottom-4 right-4"],
} as const

export function Screws({ inset = "3" }: { inset?: keyof typeof screwCorners }) {
  return (
    <>
      {screwCorners[inset].map((c, i) => (
        <Screw key={c} className={c} angle={[20, 75, 130, 160][i]} />
      ))}
    </>
  )
}

/** A physical key: a raised plastic button that goes down when pressed. */
export function PanelKey({ className, tone = "plain", ...props }: React.ComponentProps<"button"> & { tone?: "plain" | "go" }) {
  return (
    <button
      type="button"
      data-tone={tone}
      className={cn(
        "bevel-raised inline-flex h-11 items-center whitespace-nowrap justify-center rounded-(--bevel-radius-sm) px-5 text-[15px] font-semibold outline-none select-none",
        "[--bevel-material:#5B636B] text-[#EEF1F3] data-[tone=go]:[--bevel-material:#C9A44A] data-[tone=go]:text-[#2A2208]",
        "transition-[translate,box-shadow] duration-(--bevel-duration-press) active:translate-y-0.5 active:bevel-pressed",
        "focus-visible:ring-3 focus-visible:ring-[#E9D28A]/60 disabled:opacity-50",
        className
      )}
      {...props}
    />
  )
}

/** A backlit status strip: one line of glowing text behind glass. */
export function StatusStrip({ className, children }: { className?: string; children: React.ReactNode }) {
  return (
    <div
      className={cn(
        "relative overflow-hidden rounded-(--bevel-radius-sm) bg-[#0E1A17] px-4 py-3 shadow-(--bevel-depth-recessed)",
        className
      )}
    >
      <p className="text-[17px] sm:truncate tracking-wide text-[#8FF0CF] [text-shadow:0_0_8px_rgb(143_240_207/0.55)]">{children}</p>
      <span aria-hidden className="bevel-glass absolute inset-0" />
    </div>
  )
}
```

### `registry/bevel/blocks/review-desk/shared/events.ts`

```ts
/**
 * Announces a milestone in a demo (a merge, a top-up) as a DOM event. It
 * does nothing on its own; a page that wants to count milestones listens
 * for "bevel:demo". The Bevel site forwards them to its analytics.
 */
export function demoMilestone(demo: string, action: string) {
  if (typeof window === "undefined") return
  window.dispatchEvent(new CustomEvent("bevel:demo", { detail: { demo, action } }))
}
```

### `registry/bevel/blocks/review-desk/shared/fonts.ts`

```ts
import { Barlow_Condensed, Libre_Caslon_Text, Overpass, Source_Serif_4 } from "next/font/google"

/** Each demo product has its own type voice, drawn from its world. */

/** Relay: console legends, the condensed grotesk of control-room engraving. */
export const consoleFace = Barlow_Condensed({ subsets: ["latin"], weight: ["500", "600", "700"], variable: "--font-console" })

/** Meterline: a ledger serif, for a desk where money is written down. */
export const ledgerFace = Libre_Caslon_Text({ subsets: ["latin"], weight: ["400", "700"], variable: "--font-ledger" })

/** Shipyard: highway-sign lettering, for a board read from across a room. */
export const signFace = Overpass({ subsets: ["latin"], weight: ["400", "600", "800"], variable: "--font-sign" })

/** Casefile: a document serif, for papers on a desk. */
export const documentFace = Source_Serif_4({ subsets: ["latin"], weight: ["400", "600", "700"], variable: "--font-document" })
```

### `registry/bevel/blocks/review-desk/page.tsx`

```tsx
import { CasefileApp } from "@/components/bevel-demos/casefile/casefile-app"

export default function Page() {
  return <CasefileApp />
}
```
