import { Fragment, createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { registerCredentialedOrigin } from '../utilities/credentials.js'
import formatSize from '../utilities/formatSize.js'
import { ROW_HEIGHT, ROW_FILL } from '../molecules/OptionRow.jsx'
import { glyphSize } from '../hooks/glyphLadders.js'
import { Icon } from '@kolkrabbi/kol-icons'
import Button from '../atoms/Button.jsx'
import Divider from '../atoms/Divider.jsx'
import Input from '../atoms/Input.jsx'
import Tag from '../atoms/Tag.jsx'
import IconFrame from '../atoms/IconFrame.jsx'
import CloseButton from '../atoms/CloseButton.jsx'
import ActionButton from '../molecules/ActionButton.jsx'
import SizeOrDownload from '../atoms/SizeOrDownload.jsx'
import ToggleCheckbox from '../atoms/ToggleCheckbox.jsx'
import ViewToggle from '../atoms/ViewToggle.jsx'
import Dropdown from '../molecules/Dropdown.jsx'
import ContentCard from '../molecules/ContentCard.jsx'
import MediaTile from '../molecules/MediaTile.jsx'
import RowMenuButton from '../molecules/RowMenuButton.jsx'
import ContentRow from '../molecules/ContentRow.jsx'
import SortControls from '../molecules/SortControls.jsx'
import SearchInput from '../molecules/SearchInput.jsx'
import TabsRow, { TABBAR_H } from '../molecules/TabsRow.jsx'
import { filterMedia, rankMedia } from '../utilities/mediaSearch.js'
import { useMasthead, mastheadTitleClass } from '../utilities/masthead.js'
import { MenuItem, MenuDropdownItem, MenuDropdownDivider } from '../molecules/MenuItem.jsx'
import { Tooltip } from '../utilities/Popover.jsx'
import ContextMenu, { useContextMenu } from '../molecules/ContextMenu.jsx'
import useMarquee from '../hooks/useMarquee.js'
import useLongPress from '../hooks/useLongPress.js'
import useMediaQuery from '../hooks/useMediaQuery.js'
import KindPreview from '../molecules/KindPreview.jsx'
import MediaPlayer from '../molecules/MediaPlayer.jsx'
import Slider from '../molecules/Slider.jsx'
import QuickLookFrame from '../molecules/QuickLookFrame.jsx'
import { formatLength } from '../molecules/PlayTile.jsx'
import FullscreenOverlay from '../utilities/FullscreenOverlay.jsx'
import { useModal } from '../molecules/Modal.jsx'
import ContentFilters from './ContentFilters.jsx'
import DocumentEditor from './DocumentEditor.jsx'
import { listDrafts, moveDrafts, DRAFTS_EVENT } from '../utilities/localDrafts.js'
import { parseFrontmatter } from '@kolkrabbi/kol-markdown'
import CommandOverlay from './CommandOverlay.jsx'
import ColumnBrowser, { isFileDrag, Preview as ColumnPreview, SelectionPreview } from './ColumnBrowser.jsx'
import SettingsPanel, { SettingsSwitch, SettingsChoice, SettingsMulti, SettingsFooter } from './SettingsPanel.jsx'
import SettingsSections from './SettingsSections.jsx'
import { kindOf, extOf, KIND_LABEL, KINDS, DEFAULT_KINDS, isSystemFile, isSegment, groupSegments, groupVariants, posterFor, partition } from '../utilities/mediaKinds.js'
import { nearestRatio } from '../utilities/ratios.js'

/* taxonomy-ok: organism — the two media pages composed of DS parts. */

/**
 * MediaLibrary's two PAGES — kol-r2b2's app cut in two (MediaLibraryPages,
 * 2026-08-27; user: "wouldn't it make sense to have these much newer components
 * available in library?" — and the split, ruled the same hour: "one page for the
 * content filters, one page for folder/files, and a local gallery. Bucket is a
 * control, not a page"):
 *
 *   MediaLibraryBrowse   `variant="browse"`  — r2b2's header (the bucket Dropdown,
 *                        a lock when read-only, settings) · the crumb line with the
 *                        ROW | COLUMN toggle · ColumnBrowser (or folder rows) on the
 *                        flat key space · the count line
 *   MediaLibraryLibrary  `variant="library"` — r2b2's FileList wall, promoted
 *                        verbatim minus the app wiring: ContentFilters (FILES ·
 *                        kind chips · search · SELECT / FLAT · grid | list | off ·
 *                        SortControls · the selection bar while selecting), the
 *                        wall on ContentCard / ContentRow default, paging, the
 *                        per-bucket list cache, the stats line, the inspector
 *                        lightbox (image · MediaPlayer · DocPage via
 *                        KindPreview)
 *
 * Both take the injected `client` (`{ listMedia, mediaUrl, proxied?, buckets? }`
 * — ARCHITECTURE §3 stands: never imported) and render READ-ONLY unless the
 * client carries the write seams (`deleteObject` · `renameObject` · `downloadUrl`
 * — the admin app's; brand's read client shows none). Buckets come from
 * `client.buckets()` (kol-media-client ≥0.2.0); a client with none is one bucket
 * and no dropdown. Settings are kol-r2b2's per-bucket model (`SETTINGS_BASE`),
 * controlled through `settings` / `onSettingsChange` so a consumer persists them.
 *
 * WHAT A DATABASE BESIDE THE BUCKET ADDS (the media D1 pass, 2026-09-25 — kol-olina keeps these
 * in Cloudflare D1). Every verb is optional; a client without one hides that feature and nothing
 * else changes. Keys are the page's own (bucket keys), `bucket` is the bucket id.
 *   tags          listed objects carry `tags: string[]`; `setTags(key, tags, bucket)` writes them —
 *                 preview-pane chips, "Tags…" / "Add tags to N…" in the menu, a Tags filter
 *                 group, a Tags column under Fields
 *   documents     `readText(key, bucket) → { text }` · `writeText(key, text, bucket)` ·
 *                 `createFile(key, bucket, text?)` — Edit in the pane and in Quick Look, New document
 *                 in the menu (markdown gets a fields form over its frontmatter; svg a live picture).
 *                 DRAFTS ARE NOT THE CLIENT'S: browser memory (`utilities/localDrafts`), user ruling
 *                 2026-09-26 — only what must outlive a device goes to the database
 *   settings      `loadSettings(bucket)` · `saveSettings(bucket, settings | null)` (null = reset) —
 *                 used when the page is UNCONTROLLED; a `settings` prop still wins
 */

/* THE ROW VIEW'S COLUMNS (user 2026-09-22). OFF by default and `rowColumns` turns them on: with
 * them off a row is a name, and every fact about it is one click away in the preview. On, the
 * header and both row kinds read these same widths — drag a header divider and both move — and the
 * values sit LEFT in their column, as the reference draws them. */
const ROW_COL_DEFAULTS = { date: 96, size: 80 }
const ROW_COL_MIN = 56
const ROW_COL_MAX = 240
const rowColWidths = (settings) => ({ ...ROW_COL_DEFAULTS, ...(settings.rowColumnWidths ?? {}) })

/* ── the settings model — kol-r2b2's lib/settings.js BASE, verbatim ───────── */
export const ALL_KINDS = [...KINDS, 'segments', 'system']
export const SETTINGS_BASE = {
  kinds: [...DEFAULT_KINDS],
  flat: false,
  groupVariants: true,
  foldSegments: true,
  pageSize: 200,
  videoPreview: 'poster', // 'poster' | 'none' | 'autoload'
  layout: 'off', // 'off' | 'grid' | 'list' — off by default: the column view browses, cards load thumbnails
  folderView: 'columns', // 'rows' | 'columns'
  sortBy: 'name',
  sortDir: 'asc',
  columnHeight: 528, // px, any CSS length (forwarded to ColumnBrowser `height` untouched), or 'fill' — what is left of the window
  columnWidths: {},
  rowPreview: true, // row view's preview pane beside the list (Finder's "Show preview")
  view: 'columns', // 'columns' | 'rows' | 'grid' | 'list' — the ONE view state (2026-09-22)
  filters: false, // the filter bar above the body; off until asked for
  rowColumns: false, // row view's Date/Size columns + their header; off, a row is just its name
  rowColumnWidths: { ...ROW_COL_DEFAULTS }, // px, dragged on the header's own dividers
  countLine: 'auto', // 'auto' | 'on' | 'off' — the `N folders · N files · size` line under the body; auto = on at a desk, off on a phone or a touch device (apps review 2026-09-29)
}

/* THE FOUR VIEWS (the merge, user 2026-09-22: "they both just display files"). Columns and rows
 * walk the tree; grid and list are the same files as a wall. One switch, one surface, one listing —
 * `BROWSE · FILES` was two pages pretending to be two products. */
/* THREE, NOT FOUR (user 2026-09-23: *"list view is the same as row view now..so redundant"*). It
 * was: the wall's list is a row per file with a thumbnail, which is what the row view became the
 * moment it took the pane, the columns and the preview. A stored `list` lands on `rows`. */
const VIEW_OPTIONS = [
  { value: 'columns', label: 'Columns', icon: 'columns' },
  /* `rows` in the set is a BRANCH glyph, not a row stack — the tree list takes `row` (stacked
   * bands) and the file list the four rules of `view-list`. */
  { value: 'rows', label: 'Rows', icon: 'row' },
  { value: 'grid', label: 'Grid', icon: 'grid' },
]

const LAYOUT_OPTIONS = [
  { value: 'grid', label: 'Grid', icon: 'grid' },
  { value: 'list', label: 'List', icon: 'view-list' },
  { value: 'off', label: 'Off', icon: 'eye-off' },
]
/* the stack view the `···` switches (item 15) — the same two ColumnBrowser
 * carries below `md`, named as the references name them */
const STACK_VIEW_OPTIONS = [
  { value: 'list', label: 'List' },
  { value: 'grid', label: 'Icons' },
]

const SORT_OPTIONS = [
  { value: 'name', label: 'Name' },
  { value: 'date', label: 'Date' },
  { value: 'size', label: 'Size' },
  { value: 'kind', label: 'Kind' },
]

/* bytes → weight: `utilities/formatSize` since 0.178.0 — one copy, exported */
/* the pages' shared date default. Named `default*` because BOTH pages now take
 * a `formatDate` prop, and a prop of the same name would shadow this into a TDZ
 * error at the destructure. */
const defaultFormatDate = (iso) => (iso ? new Date(iso).toISOString().slice(0, 10) : '')
const isImage = (ct) => !!ct && ct.startsWith('image/')
const isVideo = (ct) => !!ct && ct.startsWith('video/')

function sortFiles(files, sortBy, sortDir) {
  const dir = sortDir === 'desc' ? -1 : 1
  return [...files].sort((a, b) => {
    if (sortBy === 'date') return dir * (new Date(a.uploaded) - new Date(b.uploaded))
    if (sortBy === 'size') return dir * (a.size - b.size)
    if (sortBy === 'kind') return dir * kindOf(a).localeCompare(kindOf(b)) || a.displayKey.localeCompare(b.displayKey)
    return dir * a.displayKey.localeCompare(b.displayKey)
  })
}

/* R2 has no real folders — "move" = rewrite the key with a folder prefix */
const moveKey = (key, prefix, folder) => {
  const clean = folder.replace(/^\/+|\/+$/g, '').trim()
  const rel = prefix ? key.slice(prefix.length) : key
  return `${prefix}${clean}/${rel}`
}

/* ── ONE hook: the bucket, its list, its settings ───────────────────────────
 * Lists from root and partitions client-side so folder counts are right; the
 * result carries the refresh key it answered so `loading` is derived. A
 * per-bucket cache makes switching back instant while the fetch replaces it. */
function useBucketLibrary({ client, bucket, defaults, settings: controlled, onSettingsChange, refreshKey = 0 }) {
  const buckets = useMemo(() => client?.buckets?.() ?? [], [client])
  const bucketMeta = buckets.find((b) => b.id === bucket) ?? buckets[0] ?? { id: null, label: '', writable: false }
  const bucketId = bucketMeta.id
  /* a signed-in bucket's files are fetched with the cookie by every preview (utilities/credentials.js, 2026-10-28) */
  if (bucketMeta.signedIn && bucketMeta.publicBase) registerCredentialedOrigin(bucketMeta.publicBase)
  const base = () => ({ ...SETTINGS_BASE, ...(defaults?.[bucketId] ?? defaults ?? {}) })
  const [own, setOwn] = useState(base)
  /* PERSONALISATION (the media D1 pass, 2026-09-25): an uncontrolled page whose client offers
   * `loadSettings(bucket)` / `saveSettings(bucket, settings | null)` keeps each person's view
   * settings THERE — kol-olina's D1 — instead of for this tab only. `null` is reset. A `settings`
   * prop still wins: a consumer that owns them is left alone, exactly as before. */
  const remote = !controlled && typeof client?.loadSettings === 'function'
  useEffect(() => {
    if (!remote) return undefined
    let live = true
    Promise.resolve(client.loadSettings(bucketId ?? undefined))
      .then((got) => { if (live) setOwn({ ...base(), ...(got ?? {}) }) })
      .catch(() => {})
    return () => { live = false }
  }, [remote, client, bucketId]) // eslint-disable-line react-hooks/exhaustive-deps
  const settings = controlled ?? own
  const setSettings = (next) => {
    if (next === null) {
      const back = base()
      if (!controlled) setOwn(back)
      if (remote) Promise.resolve(client.saveSettings?.(bucketId ?? undefined, null)).then((got) => got && setOwn({ ...back, ...got })).catch(() => {})
      onSettingsChange?.(back)
      return
    }
    if (!controlled) setOwn(next)
    if (remote) Promise.resolve(client.saveSettings?.(bucketId ?? undefined, next)).catch(() => {})
    onSettingsChange?.(next)
  }
  const [loaded, setLoaded] = useState({ key: null, bucket: null, objects: [], folders: {}, error: null })
  const [cache, setCache] = useState({})
  /* `reload()` — a write the PAGE made itself (a new document, a tag on a folder) re-lists without
   * waiting for the consumer to bump `refreshKey` */
  const [nonce, setNonce] = useState(0)
  const loadKey = `${refreshKey}.${nonce}`
  useEffect(() => {
    if (!client) return undefined
    let cancelled = false
    const controller = new AbortController()
    /* the folder rows (`folderInfo`, a D1 read) come beside the listing: tags and favourites a
     * folder carries, which a key-prefix bucket has nowhere to keep */
    Promise.all([
      client.listMedia('', { signal: controller.signal, bucket: bucketId ?? undefined }),
      Promise.resolve(client.folderInfo?.(bucketId ?? undefined)).catch(() => ({})),
    ])
      .then(([objs, folders]) => { if (cancelled) return; setLoaded({ key: loadKey, bucket: bucketId, objects: objs, folders: folders ?? {}, error: null }); setCache((c) => ({ ...c, [bucketId]: objs })) })
      .catch((e) => { if (!cancelled && e.name !== 'AbortError') setLoaded({ key: loadKey, bucket: bucketId, objects: [], folders: {}, error: e.message }) })
    return () => { cancelled = true; controller.abort() }
  }, [client, loadKey, bucketId]) // eslint-disable-line react-hooks/exhaustive-deps
  const reload = () => setNonce((n) => n + 1)
  const ready = loaded.key === loadKey && loaded.bucket === bucketId
  const objects = ready ? loaded.objects : (cache[bucketId] ?? [])
  const setObjects = (fn) => setLoaded((prev) => ({ ...prev, objects: fn(prev.objects) }))
  const folderInfo = loaded.folders ?? {}
  const setFolderInfo = (fn) => setLoaded((prev) => ({ ...prev, folders: fn(prev.folders ?? {}) }))
  const error = ready ? loaded.error : null
  /* a PATH resolves to the row's `storageKey` — the object never moves, rename and move edit the
   * row (MediaLibraryUrlOnStorageKey, kol-website 2026-10-13); no `storageKey`, the path */
  const storageOf = useMemo(() => new Map(objects.filter((o) => o.storageKey).map((o) => [o.key, o.storageKey])), [objects])
  const mediaUrl = (key) => client?.mediaUrl?.(storageOf.get(key) ?? key, bucketId ?? undefined) ?? key
  const downloadUrl = (key) => client?.downloadUrl?.(key, bucketId ?? undefined) ?? mediaUrl(key)
  const writable = !!bucketMeta.writable && !!(client?.deleteObject || client?.renameObject)
  /* A CLIENT WITH NO `mediaUrl` HAS NO URLS (kol-fxr 2026-10-08: a stored preset is not a fetchable
   * object) — Copy URL, Download and Quick Look's fetch all go, as write verbs go without a writer. */
  const hasUrl = typeof client?.mediaUrl === 'function'
  return { buckets, bucketMeta, bucketId, settings, setSettings, objects, setObjects, folderInfo, setFolderInfo, reload, error, ready, mediaUrl, downloadUrl, writable, hasUrl }
}

/* ── shared pieces, kol-r2b2's ─────────────────────────────────────────────── */

/* The frame is square until the file reports its size, then snaps to its nearest preset. A photo
 * near a preset fills it (`cover`, a few percent cropped); a vector or anything past the ladder's
 * ends — a 2.8:1 wordmark, an 11:1 nav logo — shows whole (`contain`, kol-client-olina 2026-09-25). */
function ImageFrame({ src, vector }) {
  const [frame, setFrame] = useState({ ratio: '1 / 1', contain: !!vector })
  const onLoad = (e) => {
    const { naturalWidth: w, naturalHeight: h } = e.target
    const r = w && h ? w / h : 1
    setFrame({ ratio: nearestRatio(w, h), contain: vector || r > 16 / 9 || r < 9 / 16 })
  }
  return (
    <div className="w-full rounded overflow-hidden flex items-center justify-center" style={{ aspectRatio: frame.ratio }}>
      <img src={src} alt="" className={`w-full h-full ${frame.contain ? 'object-contain' : 'object-cover'}`} loading="lazy" onLoad={onLoad} />
    </div>
  )
}


/* TAGS IN THE PREVIEW PANE (the media D1 pass, 2026-09-25). The file's tags as removable chips and
 * one field that adds on Enter — comma-separated adds several. Tags are the CLIENT's (`setTags`);
 * without it, or on a read-only bucket, the chips show and nothing edits them. */
function TagEditor({ tags = [], onChange, suggestions = [] }) {
  const listId = useMemo(() => `kol-tags-${Math.random().toString(36).slice(2, 8)}`, [])
  const add = (raw) => {
    const next = raw.split(',').map((t) => t.trim().toLowerCase()).filter(Boolean)
    if (next.length) onChange([...new Set([...tags, ...next])])
  }
  if (!onChange && !tags.length) return null
  return (
    <div className="flex flex-col gap-2">
      <span className="kol-mono-12 text-fg-48">Tags</span>
      {tags.length > 0 && (
        <div className="flex flex-wrap gap-1">
          {tags.map((t) => (
            <Tag key={t} variant="secondary" size="sm" onRemove={onChange ? () => onChange(tags.filter((x) => x !== t)) : undefined}>{t}</Tag>
          ))}
        </div>
      )}
      {/* the bucket's own tags as suggestions — native autocomplete, so a typo does not fork the vocabulary */}
      {onChange && <Input size="xs" value="" placeholder="Add a tag" aria-label="Add a tag" onCommit={add} list={listId} />}
      {onChange && (
        <datalist id={listId}>
          {suggestions.filter((t) => !tags.includes(t)).map((t) => <option key={t} value={t} />)}
        </datalist>
      )}
    </div>
  )
}

/* THE ROWS VIEW'S STEP ON THE CONTROL RAMP (user 2026-09-30) — the same `rowSize` the columns take,
 * handed down by context rather than threaded through every subtree level. These rows keep their
 * own anatomy (twisty, drag hit, table columns) and take the ramp's box and glyph, OptionRow's
 * fills (picked = the pressed white, the folder you are in = the trail) and the one item type,
 * `kol-item-name`, that the columns and the grid use. */
const RowSize = (globalThis[Symbol.for('@kolkrabbi/kol-component/RowSize')] ??= createContext('md'))

/* `depth` indents a row under the folder it was expanded from; `expanded`/`onToggle` draw the
 * disclosure twisty. A folder row without `onToggle` keeps the old behaviour exactly — one chevron
 * that navigates — so nothing that already renders these moves. */
function FolderRow({ name, icon = 'folder', onClick, onDoubleClick, depth = 0, expanded, onToggle, meta, cols, onContextMenu, drag, dropFiles, selected, current, markKey }) {
  const [over, setOver] = useState(false)
  const size = useContext(RowSize)
  /* A FOLDER IS A DROP TARGET. `drag` carries the page's move verb and the path this row is; the
   * row highlights only while something is actually over it, so an accidental hover reads as
   * nothing. Without `drag` every handler below is undefined and the row is exactly what it was. */
  /* `dropFiles(files)` is the OS half: desktop files dropped here go to the consumer's upload. */
  const takesFiles = (e) => dropFiles && isFileDrag(e)
  /* A CLICK TAKES THE WHOLE ROW, A DRAG STARTS WHERE YOU PRESSED (user 2026-09-23: *"the example
   * I used before … was more about the ability to make a marquee selection, not to make it difficult
   * to select row items"*). Click, double-click and ⌘/⇧-click land anywhere on the row. The icon and
   * name (`data-hit`) carry the drag-to-move and the pointer; a press-and-drag anywhere else on the
   * row draws the marquee (`data-hit-zone`, `useMarquee`). The DROP stays on the whole row. */
  const hitDrag = drag ? { draggable: true, onDragStart: (e) => { e.stopPropagation(); drag.onDragStart(e, drag.path) } } : {}
  const dropProps = drag || dropFiles ? {
    onDragOver: (e) => { if (takesFiles(e) || (drag && !isFileDrag(e) && drag.canDrop(drag.path))) { e.preventDefault(); setOver(true) } },
    onDragLeave: () => setOver(false),
    /* A ROW THAT CANNOT TAKE THE DROP LETS IT BUBBLE (the file-onto-file bug, 2026-09-25): the
     * enclosing column / list allows the drag, so the drop still fires HERE, and moving into a
     * FILE's key renamed the dragged file. Unguarded, it skipped `canDrop`; now the container
     * takes it — dropped on a file, it lands in that file's folder, as Finder does. */
    onDrop: (e) => {
      setOver(false)
      if (takesFiles(e)) { e.preventDefault(); e.stopPropagation(); dropFiles(e.dataTransfer.files); return }
      if (!drag || isFileDrag(e) || !drag.canDrop(drag.path)) return
      e.preventDefault(); e.stopPropagation()
      drag.onDrop(drag.path)
    },
  } : {}
  return (
    /* THE COLUMN BROWSER'S ROW (user 2026-09-22): `kol-column-browser-row` carries the 4px pill
     * inset, the selected fill and the drop fill; no divider, as the columns have none. */
    <li onContextMenu={onContextMenu} {...dropProps} data-marquee-key={markKey} data-hit-zone data-drop-over={over || undefined} className={`kol-column-browser-row flex items-center gap-3 border border-transparent px-3 ${ROW_HEIGHT[size]} transition-colors${selected ? ` is-selected ${ROW_FILL.selected}` : ''}${current ? ' is-current' : ''}${current && !selected ? ` ${ROW_FILL.trail}` : ''}`} style={{ paddingLeft: depth * 20 + 12 }} onClick={onClick} onDoubleClick={onDoubleClick}>
      {onToggle ? (
        <button type="button" aria-label={expanded ? `Collapse ${name}` : `Expand ${name}`} aria-expanded={!!expanded}
          onClick={(e) => { e.stopPropagation(); onToggle() }}
          className="w-4 h-4 shrink-0 flex items-center justify-center text-oq-32 hover:text-fg-default transition-transform"
          style={{ transform: expanded ? 'rotate(90deg)' : 'none' }}>
          <Icon name="chevron-right" size={12} />
        </button>
      ) : <span className="w-4 shrink-0" />}
      {/* THE FOLDER GLYPH IS THE ONE ICON LEFT, so it fills its box (user 2026-09-23: *"only folder
          should use icon, and it should be bigger"*). Every other kind now renders its own bytes. */}
      <span data-hit {...hitDrag} className="kol-row-hit flex items-center gap-3 min-w-0">
        <span className={`shrink-0 flex items-center justify-center${selected ? '' : ' text-oq-48'}`}><Icon name={icon} size={glyphSize(size)} /></span>
        {/* NO TRAILING SLASH (user 2026-09-22). `partition` hands folders back as `audio/` because a
            slash is what marks one in a flat key space; that is storage's spelling, not a label, and
            `ColumnBrowser` has always stripped it. The glyph says "folder". */}
        <span className={`kol-item-name min-w-0 truncate${selected ? '' : ' text-fg-default'}`}>{name.replace(/\/$/, '')}</span>
      </span>
      <span className="flex-1" />
      {/* THE FOLDER'S COUNT SITS IN THE DATE COLUMN and the size column takes a dash — the columns
          line up or the header above them is a lie. The trailing chevron went with them: it
          promised a click that opens, and a click now SELECTS (opening is a double-click). */}
      {cols && <>
        {cols.tags != null && <span className="shrink-0" style={{ width: cols.tags }} />}
        <span className={`kol-item-name ${selected ? "opacity-60" : "text-fg-32"} shrink-0 truncate`} style={{ width: cols.date }}>{meta}</span>
        <span className={`kol-item-name ${selected ? "opacity-60" : "text-fg-24"} shrink-0 truncate`} style={{ width: cols.size }}>—</span>
      </>}
      <RowMenuButton onOpen={onContextMenu} className="-my-1 -mr-1" />
    </li>
  )
}

/* One expanded level, drawn inline under its folder, recursing for anything expanded beneath it —
 * the disclosure behaviour a list view is expected to have. Depth only indents; the data comes from
 * the same `rowLevel` the top level uses, so an expanded branch cannot disagree with the columns. */
function RowSubtree({ level, depth, rowLevel, expanded, onToggle, onOpenFolder, onPickFolder, onOpenFile, onPickFile, formatDate, folderMeta, thumbnailFor, cols, onRowContextMenu, dragFor, dropFilesTo, pickedKey, currentKey, selectRow, order, selectedKeys }) {
  const { folders, files } = rowLevel(level)
  return (
    <>
      {folders.map((f) => {
        const path = level + f
        const open = expanded.has(path)
        return (
          <Fragment key={path}>
            <FolderRow
              name={f} depth={depth} expanded={open}
              markKey={path}
              meta={folderMeta?.(path) || undefined}
              cols={cols}
              onToggle={() => onToggle(path)}
              onContextMenu={(e) => onRowContextMenu?.(e, { type: 'folder', path, targets: selectedKeys?.has(path) ? [...selectedKeys] : [path] })}
              drag={dragFor?.(path)}
              dropFiles={dropFilesTo?.(path)}
              selected={pickedKey === path || !!selectedKeys?.has(path)}
              current={currentKey === path}
              /* SELECTION IS THE SAME HERE AS AT THE ROOT (user 2026-09-23). These rows had no
               * `selectRow` at all, so ⌘-click picked instead of adding — which is why a drag
               * carried one item however many looked lit. */
              onClick={(e) => { if (!selectRow?.(path, e, order)) onPickFolder(path) }}
              onDoubleClick={() => onOpenFolder(path)}
            />
            {open && <RowSubtree
              level={path} depth={depth + 1} rowLevel={rowLevel} expanded={expanded} onToggle={onToggle}
              onOpenFolder={onOpenFolder} onPickFolder={onPickFolder} onOpenFile={onOpenFile} onPickFile={onPickFile} onRowContextMenu={onRowContextMenu} dragFor={dragFor} dropFilesTo={dropFilesTo} pickedKey={pickedKey} currentKey={currentKey} cols={cols}
              selectRow={selectRow} order={order} selectedKeys={selectedKeys}
              formatDate={formatDate} folderMeta={folderMeta} thumbnailFor={thumbnailFor}
            />}
          </Fragment>
        )
      })}
      {files.map((o) => (
        <FileRow key={o.key} o={o} depth={depth} formatDate={formatDate} thumb={thumbnailFor?.(o)} cols={cols}
          selected={pickedKey === o.key || !!selectedKeys?.has(o.key)}
          onContextMenu={(e) => onRowContextMenu?.(e, { type: 'file', path: o.key, o, targets: selectedKeys?.has(o.key) ? [...selectedKeys] : [o.key] })}
          drag={dragFor?.(o.key)}
          onClick={(e) => { if (!selectRow?.(o.key, e, order)) onPickFile(o) }}
          onDoubleClick={() => onOpenFile(o)} />
      ))}
    </>
  )
}

/* The file half of the row view. Row view listed FOLDERS ONLY and dropped `files` on the floor, so
 * `#img/` drew two rows where the column browser drew four — the two folders, the empty folder the
 * columns knew about, and a file. A list view that cannot show a file is not a list view. */
function FileRow({ o, onClick, onDoubleClick, depth = 0, formatDate, thumb, cols, onContextMenu, drag, selected, markKey }) {
  const size = useContext(RowSize)
  const dragProps = drag ? { draggable: true, onDragStart: (e) => { e.stopPropagation(); drag.onDragStart(e, o.key) } } : {}
  return (
    <li onContextMenu={onContextMenu} data-marquee-key={markKey ?? o.key} data-hit-zone className={`kol-column-browser-row flex items-center gap-3 border border-transparent px-3 ${ROW_HEIGHT[size]} transition-colors${selected ? ` is-selected ${ROW_FILL.selected}` : ''}`} style={{ paddingLeft: depth * 20 + 12 }} onClick={onClick} onDoubleClick={onDoubleClick}>
      <span className="w-4 shrink-0" />
      {/* the hit — icon and name, as in the folder row above */}
      <span data-hit {...dragProps} className="kol-row-hit flex items-center gap-3 min-w-0">
        <span className={`shrink-0 flex items-center justify-center overflow-hidden rounded${selected ? '' : ' text-oq-48'}`} style={{ width: glyphSize(size), height: glyphSize(size) }}>
          {/* a file with no thumbnail draws its glyph at the FOLDER's size — one icon column (user 2026-09-25) */}
          {thumb ?? <Icon name={kindOf(o) === 'image' ? 'image' : 'file'} size={glyphSize(size)} />}
        </span>
        <span className={`kol-item-name min-w-0 truncate${selected ? '' : ' text-fg-default'}`}>{o.displayKey ?? o.displayName ?? o.key.split('/').pop()}</span>
      </span>
      <span className="flex-1" />
      {cols && <>
        {cols.tags != null && <span className={`kol-item-name ${selected ? "opacity-60" : "text-fg-48"} shrink-0 truncate`} style={{ width: cols.tags }}>{(o.tags ?? []).map((t) => `#${t}`).join(' ')}</span>}
        <span className={`kol-item-name ${selected ? "opacity-60" : "text-fg-32"} shrink-0 truncate`} style={{ width: cols.date }}>{formatDate?.(o.uploaded)}</span>
        <span className={`kol-item-name ${selected ? "opacity-60" : "text-fg-32"} shrink-0 truncate`} style={{ width: cols.size }}>{formatSize(o.size)}</span>
      </>}
      <RowMenuButton onOpen={onContextMenu} className="-my-1 -mr-1" />
    </li>
  )
}

/* A FOLDER HAS A PREVIEW TOO (user 2026-09-22, against Finder: selecting a folder fills the pane).
 * The columns never needed one — stepping into a folder IS its preview there — so this is the row
 * view's, and it is the page's rather than ColumnBrowser's because the counts come off the same
 * `objects` the page already holds. Same shell as the file preview, so the pane never shifts. */
function FolderPreview({ path, objects, width, formatDate, details }) {
  const inside = objects.filter((o) => o.key.startsWith(path))
  const bytes = inside.reduce((n, o) => n + (o.size ?? 0), 0)
  const last = inside.reduce((t, o) => (o.uploaded && o.uploaded > t ? o.uploaded : t), '')
  const facts = [
    ['Kind', 'folder'],
    ['Items', `${inside.length}`],
    ['Size', formatSize(bytes)],
    ...(last ? [['Newest', formatDate(last) || '—']] : []),
  ]
  return <ContainerPreview name={path.replace(/\/$/, '').split('/').pop()} icon="folder" facts={facts} width={width} details={details} />
}

/* ONE PREVIEW FOR EVERYTHING THAT HOLDS THINGS — a folder, a bucket, the title (user 2026-09-25:
 * selecting a bucket or MEDIA left the pane empty). The glyph, the name, the facts; only the
 * glyph and the facts differ. */
function ContainerPreview({ name, icon, facts, width, details }) {
  return (
    <div className="kol-column-browser-preview shrink-0 overflow-y-auto p-4 flex flex-col gap-4" style={{ width }}>
      {/* the folder fills its box with the tile's inset (user 2026-09-24: a 64px glyph floated in
          a 300px square while the grid's folder tile fills its own) */}
      <div className="w-full aspect-square bg-oq-04 rounded flex items-center justify-center p-[5%]">
        <Icon name={icon} size="100%" className="text-oq-48" />
      </div>
      <p className="kol-mono-12 text-fg-default break-all">{name}</p>
      <dl className="flex flex-col gap-1">
        {facts.map(([k, v]) => (
          <div key={k} className="flex justify-between gap-4 kol-mono-12">
            <dt className="text-fg-48">{k}</dt>
            <dd className="text-fg-default text-right break-all">{v}</dd>
          </div>
        ))}
      </dl>
      {/* a folder's own rows — favourite, tags (media D1 plan v2) — an island, like the file pane's */}
      {details && <div onClick={(e) => e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()}>{details}</div>}
    </div>
  )
}

/* THE COLUMN HEADER — Name · Date · Size, each one the sort control for its own column (user
 * 2026-09-22). Unlabelled meta at the end of a row is a number nobody can read a column of; a
 * header names them AND gives them the one behaviour a labelled column implies. Clicking the
 * active column flips the direction, which is what every file manager does. It writes the SAME
 * `sortBy`/`sortDir` the files wall's own SortControls write, so the two views cannot disagree.
 *
 * FINDER'S SHAPE, on the user's read of it (2026-09-22): the header sits ABOVE the pane rather
 * than inside it — it labels the list, it is not a row of it — the labels are sentence case, and a
 * hairline divides each column from the one before it. `PAD` is the rows' own inset (the pane's
 * 1px border + the row pill's 4px margin + the row's 12px padding), so a label lands over the
 * column it names; it is derived here rather than typed, because those three numbers are the ones
 * that move. */
const ROW_PAD = 1 + 4 + 12

function RowHeader({ sortBy = 'name', sortDir = 'asc', onSort, width, cols, onResize }) {
  const label = (key, text) => {
    const active = sortBy === key
    return (
      <button type="button" onClick={() => onSort(key, active && sortDir === 'asc' ? 'desc' : 'asc')}
        aria-sort={active ? (sortDir === 'desc' ? 'descending' : 'ascending') : 'none'}
        className={`kol-helper-12 cursor-pointer inline-flex items-center gap-1 w-full transition-colors ${active ? 'text-fg-default' : 'text-fg-48 hover:text-fg-default'}`}>
        {text}
        {active && <Icon name={sortDir === 'desc' ? 'arrow-up' : 'arrow-down'} size={9} />}
      </button>
    )
  }
  /* EACH DIVIDER RESIZES THE COLUMN TO ITS RIGHT — those are the fixed ones; Name takes whatever is
   * left, so there is nothing to drag on its side. Pointer capture, no library, same gesture the
   * column browser's own edges use. */
  const handle = (key) => (
    <span
      role="separator" aria-orientation="vertical" aria-label={`Resize ${key} column`}
      className="cursor-col-resize"
      /* inline, not utilities: it must be OUT OF FLOW or it pushes the label 8px off the column
         it names — which is exactly what it did */
      style={{ position: 'absolute', top: 0, bottom: 0, left: -4, width: 8 }}
      onPointerDown={(e) => {
        e.preventDefault()
        e.currentTarget.setPointerCapture(e.pointerId)
        const startX = e.clientX
        const base = cols[key]
        const move = (ev) => onResize(key, Math.max(ROW_COL_MIN, Math.min(ROW_COL_MAX, Math.round(base - (ev.clientX - startX)))))
        const up = () => { window.removeEventListener('pointermove', move); window.removeEventListener('pointerup', up) }
        window.addEventListener('pointermove', move)
        window.addEventListener('pointerup', up)
      }}
    />
  )
  const column = (key, text) => (
    /* no left padding: the label shares its column's left edge with the values under it */
    <div key={key} className="relative shrink-0 border-l" style={{ width: cols[key], borderColor: 'var(--kol-oq-08)' }}>
      {handle(key)}
      {label(key, text)}
    </div>
  )
  return (
    /* the right padding carries TWO extra pixels of borders — the pane's own and the cell's divider
       — which is what a label sitting two pixels right of its column looks like */
    <div className="flex items-center gap-3 shrink-0" style={{ width, paddingLeft: ROW_PAD, paddingRight: ROW_PAD + 2 }}>
      <span className="w-4 shrink-0" aria-hidden="true" />
      <span className="w-8 shrink-0" aria-hidden="true" />
      <span className="flex-1 min-w-0">{label('name', 'Name')}</span>
      {/* Tags does not sort — a file has several — so its header is a plain label */}
      {cols.tags != null && (
        <div className="relative shrink-0 border-l" style={{ width: cols.tags, borderColor: 'var(--kol-oq-08)' }}>
          {handle('tags')}
          <span className="kol-helper-12 text-fg-48">Tags</span>
        </div>
      )}
      {column('date', 'Date')}
      {column('size', 'Size')}
    </div>
  )
}

/* THE KINDS THAT OPEN AS TEXT — what the document editor takes (media D1 plan v2, 2026-09-26).
 * An SVG is text too: it opens with its picture beside it. Above EDIT_CAP a file is not loaded into
 * an editor at all — a textarea holding megabytes is a frozen tab, not an edit. */
const TEXT_KINDS = new Set(['markdown', 'json', 'yaml', 'text', 'code'])
const EDIT_CAP = 1_048_576
const editKindOf = (o) => (o.contentType === 'image/svg+xml' || extOf(o.key) === 'svg' ? 'svg' : TEXT_KINDS.has(kindOf(o)) ? kindOf(o) : null)

/* The editor for a file that exists: reads its text through the client, saves through it. Drafts
 * are the DocumentEditor's (browser memory), keyed by bucket + key. */
function FileEditorHost({ o, client, bucket, onClose, onSaved, assets }) {
  const [text, setText] = useState(null)
  useEffect(() => {
    let live = true
    client.readText(o.key, bucket).then((r) => live && setText(r?.text ?? '')).catch(() => live && setText(''))
    return () => { live = false }
  }, [o.key]) // eslint-disable-line react-hooks/exhaustive-deps
  return (
    <DocumentEditor name={o.displayKey ?? o.displayName ?? o.key.split('/').pop()} kind={editKindOf(o)} text={text}
      savedAt={o.uploaded ? Date.parse(o.uploaded) : 0} draft={{ bucket, key: o.key }} assets={assets}
      onSave={async (next) => { const r = await client.writeText(o.key, next, bucket); onSaved?.(o.key, r, next) }}
      onClose={onClose} />
  )
}

/**
 * MediaInspector — View one file at full size. the full-screen viewer for one file in a set: image, video
 * (scrubbable, audio on), audio, or a document page, in one `QuickLookFrame`, with
 * prev / next fixed at the viewport edges.
 *
 * PUBLIC SINCE 2026-09-04 (user ruling, off `ColumnBrowserMobileViews`: the
 * mobile viewers are the DS's). It already existed and already rendered both
 * of the views that ticket drew — it was simply module-private, so every
 * consumer that needed a phone viewer had to hand-roll one, which is the
 * outcome the ticket named. Same unweld as the rulers a day earlier: the
 * component was right, its visibility was not.
 *
 * Shell is `FullscreenOverlay` (scrim, Escape, backdrop dismiss, scroll lock).
 * The stage is kol-r2b2's MediaLightbox verbatim. Not `MediaViewer`, which is a
 * gallery — muted, looping, no controls, image and video only.
 *
 * @param {Array<Object>} files - The set being paged through
 * @param {number} index - Which one is open
 * @param {Function} onClose - Dismiss
 * @param {Function} onPrev - Previous; also ArrowLeft
 * @param {Function} onNext - Next; also ArrowRight
 * @param {Function} mediaUrl - `(key) => url` — the consumer's media resolver
 * @param {Function} downloadUrl - `(key) => url` for the download control
 * @param {Set|Object} keySet - The bucket's key set, used to find a video's poster
 * @param {boolean} showNav - ‹ › and n / N in the header (default on); off when not paging a selection
 * @param {Function} onEdit - `(file) => void` — an Edit action in the header for a text file (markdown · json · yaml · text · code); absent, no Edit
 */
export function MediaInspector({ files, index, onClose, onPrev, onNext, mediaUrl, downloadUrl, keySet, showNav = true, onEdit, hasUrl = true }) {
  const o = files[index]
  const [dims, setDims] = useState(null)
  /* the window's size survives paging — Finder keeps it — so it lives here, not in the frame,
   * which remounts whenever the kind changes */
  const [size, setSize] = useState(null)
  const shownDims = dims && dims.key === o?.key ? dims : null
  /* fill width is per file: paging lands on a key that is not this one, so it opens in fit */
  const [fillKey, setFillKey] = useState(null)
  const fill = fillKey != null && fillKey === o?.key
  useEffect(() => {
    const onKey = (e) => { if (e.shiftKey) return; if (e.key === 'ArrowLeft') onPrev(); if (e.key === 'ArrowRight') onNext() }
    window.addEventListener('keydown', onKey)
    return () => window.removeEventListener('keydown', onKey)
  }, [onPrev, onNext])
  if (!o) return null
  const kind = kindOf(o)
  const poster = kind === 'video' ? posterFor(o.key, keySet) : null
  /* ONE WINDOW FOR EVERY KIND (user 2026-09-23: *"put them in a container LIKE FINDER"*). The
   * name, the facts and the download ride the window's header; the transport, when there is one,
   * its footer. They used to be a caption line under whatever the kind drew. */
  const facts = [
    formatSize(o.size),
    shownDims?.w && `${shownDims.w} × ${shownDims.h} px`,
    shownDims?.len && formatLength(shownDims.len),
  ].filter(Boolean).join(' · ')
  const frame = {
    title: o.displayKey ?? o.displayName ?? o.key.split('/').pop(),
    meta: facts,
    onClose,
    /* ‹ › and n / N only when the window pages a SELECTION (user 2026-09-23: *"I only selected one
     * file, why am I getting the chevrons"*). ← → still step, as Finder's do; the header does not
     * advertise a set you did not make. `showNav` — the page passes whether several are selected. */
    nav: showNav ? { index, total: files.length, onPrev, onNext } : undefined,
    size,
    onResize: setSize,
    /* THE TWO READ VERBS (user 2026-09-23): Copy URL beside Download — the window is the one place
     * besides the right-click menu that carries them */
    /* THE CLOSE BUTTON'S IDIOM (user 2026-09-23): a filled container read as a second surface on
     * the header; bare at rest with a wash on hover, `sm` so the glyphs sit with the 12px text. */
    actions: (
      <span className="flex items-center gap-1">
        {onEdit && editKindOf(o) && (o.size ?? 0) <= EDIT_CAP && (
          <Tooltip label="Edit">
            <Button variant="nav" size="sm" iconOnly="edit" onClick={() => onEdit(o)} aria-label="Edit" />
          </Tooltip>
        )}
        {hasUrl && (
          <>
            <CopyAction label="Copy URL" text={mediaUrl(o.key)} />
            <Tooltip label="Download">
              <Button variant="nav" size="sm" iconOnly="download" href={downloadUrl(o.key)} download={o.key.split('/').pop()} aria-label="Download" />
            </Tooltip>
          </>
        )}
      </span>
    ),
  }
  return (
    /* QUICK LOOK DIMS, it does not black out — you keep the list you came from in view */
    <FullscreenOverlay open onClose={onClose} closeButton={false} scrim>
      {!hasUrl ? (
        /* nothing to fetch, so nothing to draw — said, not spun on (kol-fxr 2026-10-08) */
        <QuickLookFrame {...frame}>
          <p className="kol-helper-12 text-meta p-8">No preview</p>
        </QuickLookFrame>
      ) : isImage(o.contentType) ? (
        <QuickLookFrame {...frame}>
          {/* A SMALL VECTOR HAS NO USEFUL PIXEL SIZE (user 2026-09-24: a 32px logo.svg opened as a 32px
            * window). One under 640 wide — or with no size at all — is drawn in a 640 box, the window's
            * max if that is smaller; a big one fits the window like a raster (kol-atlas 2026-10-19: its
            * 1200×800 cards sat in the 640 box). A click toggles fit ↔ fill width; paging resets to fit. */}
          <img src={mediaUrl(o.key)} alt={o.displayKey}
            className={`kol-quicklook-media${fill ? ' kol-quicklook-media--fill' : ''}`}
            style={!fill && (extOf(o.key) === 'svg' || o.contentType === 'image/svg+xml') && !(shownDims?.w >= 640) ? { width: 'min(var(--kol-ql-max-w), 640px)', height: 'min(var(--kol-ql-media-h), 640px)' } : undefined}
            onClick={() => setFillKey(fill ? null : o.key)}
            onLoad={(e) => setDims({ key: o.key, w: e.target.naturalWidth, h: e.target.naturalHeight })} />
        </QuickLookFrame>
      ) : isVideo(o.contentType) ? (
        <MediaPlayer key={o.key} variant="video" src={mediaUrl(o.key)} poster={poster ? mediaUrl(poster) : undefined} onMeta={(m) => setDims({ key: o.key, ...m })} frame={frame} />
      ) : kind === 'audio' ? (
        <MediaPlayer key={o.key} variant="audio" src={mediaUrl(o.key)} ext={extOf(o.key)} onMeta={(m) => setDims({ key: o.key, ...m })} frame={frame} />
      ) : (
        <QuickLookFrame {...frame}>
          <KindPreview o={o} fit="sheet" urlOf={(x) => mediaUrl(x.key)} poster={poster ? mediaUrl(poster) : undefined} />
        </QuickLookFrame>
      )}
    </FullscreenOverlay>
  )
}

/* THE DISPLAY SETTINGS AS DATA (media-hub, user 2026-09-26: "missing settings sidebar? … or are
 * we saying the shell has settings different from media?"). The rows were hard-wired JSX inside this
 * drawer, so an app page could not show them and a second settings system grew beside it. Now ONE
 * builder: this drawer renders it, and an app renders the same rows on its settings page and its own
 * drawer (kol-shell `HubSettings sections`). `settings` is the page's per-bucket settings object,
 * `onChange(next)` sets it, `profile` (what the bucket holds) drives the "nothing to fold" states —
 * omit it where the listing is not at hand and those two switches stay enabled with plain hints.
 * Every control sets a default, never a gate. */
export function mediaSettingsSections({ settings, onChange, profile }) {
  const set = (patch) => onChange({ ...settings, ...patch })
  const kinds = settings.kinds ?? []
  const toggleKind = (k) => set({ kinds: kinds.includes(k) ? kinds.filter((x) => x !== k) : [...kinds, k] })
  const noVariants = profile?.variantSets === 0
  const noSegments = profile?.segments === 0
  return [
    { label: 'Structure', rowGap: 1, rows: [
      { label: 'Columns', hint: 'Finder-style columns instead of folder rows',
        render: () => <SettingsSwitch label="Columns" on={(settings.folderView ?? 'rows') === 'columns'} onChange={(v) => set({ folderView: v ? 'columns' : 'rows' })} /> },
      { label: 'Preview', hint: 'the picked file beside the rows',
        render: () => <SettingsSwitch label="Preview" on={settings.rowPreview ?? true} onChange={(v) => set({ rowPreview: v })} /> },
      /* NOT "Columns" — that switch above means Finder's column browser. These are the row view's
       * date/size FIELDS, which is what the user calls them. */
      { label: 'Fields', hint: 'date and size beside each row, with a sort header',
        render: () => <SettingsSwitch label="Fields" on={!!settings.rowColumns} onChange={(v) => set({ rowColumns: v })} /> },
      { label: 'Flat', hint: 'ignore folders, show the whole subtree',
        render: () => <SettingsSwitch label="Flat" on={settings.flat} onChange={(v) => set({ flat: v })} /> },
      { label: 'Group resolutions', hint: noVariants ? 'no resolution sets in this bucket' : profile ? `${profile.variantSets} sets — previews the smallest file` : 'previews the smallest file of a set',
        render: () => <SettingsSwitch label="Group resolutions" on={settings.groupVariants} onChange={(v) => set({ groupVariants: v })} disabled={noVariants} disabledHint="nothing to group here" /> },
      { label: 'Fold HLS segments', hint: noSegments ? 'no segments in this bucket' : profile ? `${profile.segments} segments into stream rows` : 'segments into stream rows',
        render: () => <SettingsSwitch label="Fold HLS segments" on={settings.foldSegments} onChange={(v) => set({ foldSegments: v })} disabled={noSegments} disabledHint="nothing to fold here" /> },
    ] },
    { label: 'Loading', rows: [
      { label: 'Kinds', align: 'fill',
        render: () => <SettingsMulti options={ALL_KINDS.map((k) => ({ value: k, label: KIND_LABEL[k] || k }))} selected={kinds} onToggle={toggleKind} noun="kinds" /> },
      { label: 'Page size', hint: 'entries mounted at once', align: 'fill',
        render: () => <SettingsChoice options={[{ value: 100, label: '100' }, { value: 200, label: '200' }, { value: 500, label: '500' }, { value: 0, label: 'All' }]} value={settings.pageSize} onChange={(v) => set({ pageSize: v })} /> },
      { label: 'Video preview', hint: 'poster uses the sibling image; autoload fetches the file', align: 'fill',
        render: () => <SettingsChoice options={[{ value: 'poster', label: 'Poster' }, { value: 'none', label: 'None' }, { value: 'autoload', label: 'Autoload' }]} value={settings.videoPreview} onChange={(v) => set({ videoPreview: v })} /> },
    ] },
    { label: 'Layout', rows: [
      { label: 'View', align: 'fill',
        render: () => <SettingsChoice options={[{ value: 'off', label: 'Off' }, { value: 'grid', label: 'Grid' }, { value: 'list', label: 'List' }]} value={settings.layout} onChange={(v) => set({ layout: v })} /> },
      { label: 'Sort', align: 'fill',
        render: () => <SettingsChoice options={SORT_OPTIONS} value={settings.sortBy} onChange={(v) => set({ sortBy: v })} /> },
      { label: 'Direction', align: 'fill',
        render: () => <SettingsChoice options={[{ value: 'asc', label: '↓ Asc' }, { value: 'desc', label: '↑ Desc' }]} value={settings.sortDir} onChange={(v) => set({ sortDir: v })} /> },
      { label: 'Count line', align: 'fill',
        render: () => <SettingsChoice options={[{ value: 'auto', label: 'Auto' }, { value: 'on', label: 'On' }, { value: 'off', label: 'Off' }]} value={settings.countLine ?? 'auto'} onChange={(v) => set({ countLine: v })} /> },
    ] },
  ]
}

/* Display settings for the ACTIVE bucket — kol-r2b2's drawer wiring on the DS organism (the
 * composition the user locked 2026-08-27), drawn from `mediaSettingsSections`. */
function MediaSettings({ settings, onChange, onReset, onClose, profile, settingsFooter }) {
  return (
    <SettingsPanel variant="rail" title="Display settings" onClose={onClose} footer={<SettingsFooter onReset={onReset} resetLabel="Reset preferences">{settingsFooter}</SettingsFooter>}>
      <SettingsSections sections={mediaSettingsSections({ settings, onChange, profile })} divided />
    </SettingsPanel>
  )
}

/* ── the header — bucket Dropdown · lock / the app's actions · settings ────
 *
 * THE ROW HAS TO GIVE AT 390 (StackModeChromeAndAncestors §4, kol-r2b2
 * 2026-09-04). The title was `white-space: normal` in a box the fixed `w-48`
 * dropdown had squeezed to 48px, so `KOL-R2B2` wrapped MID-TOKEN into two
 * 36px lines — 72px of header spent breaking a word in half. Neither reference
 * wraps a title.
 *
 * The dropdown's 192px was the actual cost: 192 of 390 before the title, the
 * lock and the gear have had any. It keeps that width from `md` up, where the
 * room exists, and shrinks below it. The title never wraps — it takes the room
 * left and ellipsises, which is honest where a mid-token break is not.
 *
 * `headerActions` is a REAL slot and it is narrow here: at 390 the row holds
 * the dropdown, the gear, and about ONE consumer icon. kol-r2b2 added a second
 * and pushed the gear off the right edge — they resolved it by SWAPPING a
 * control below `md` rather than adding one, which is the pattern to copy. If a
 * consumer needs more than one at this width, it belongs in a sheet, not here. */
/* A copy control in the header's idiom (the close button's: bare, a wash on hover); the tooltip says it worked. */
function CopyAction({ label, text }) {
  const [copied, setCopied] = useState(false)
  useEffect(() => {
    if (!copied) return undefined
    const t = setTimeout(() => setCopied(false), 1600)
    return () => clearTimeout(t)
  }, [copied])
  return (
    <Tooltip label={copied ? 'Copied' : label}>
      <Button variant="nav" size="sm" iconOnly={copied ? 'check' : 'copy'} aria-label={label}
        onClick={() => { navigator.clipboard?.writeText(text); setCopied(true) }} />
    </Tooltip>
  )
}

/* THE CURRENT CRUMB COPIES THE PATH (user 2026-09-23: *"could we make a hover click to copy path?
 * … just click and it copies to clipboard, maybe popover tooltip appears 'path copied'"*). It was
 * a button navigating to where you already were; now it is the one crumb that does something new.
 * The other crumbs still navigate. */
function CopyCrumb({ label, path, className }) {
  const [copied, setCopied] = useState(false)
  useEffect(() => {
    if (!copied) return undefined
    const t = setTimeout(() => setCopied(false), 1600)
    return () => clearTimeout(t)
  }, [copied])
  return (
    <Tooltip label={copied ? 'Path copied' : 'Copy path'}>
      <button type="button" className={className} onClick={() => { navigator.clipboard?.writeText(path); setCopied(true) }}>{label}</button>
    </Tooltip>
  )
}

/* THE TRASH — what delete moved aside, newest first. BUILT FROM PARTS THAT EXIST (user 2026-09-23:
 * *"follow an example, dont reinvent the wheel"*): the settings drawer's title, a
 * list in the column browser's own rows, and `SettingsFooter` for Empty trash.
 *
 * NO BUTTONS ON THE ROWS (user 2026-09-23: *"remove both x and restore and use context menu for
 * those items, as single or multiple selection"*). The items select the way files do — click, ⌘ to
 * add, ⇧ for a run — and right-click offers Restore and Delete forever for the row, or for the
 * whole selection when the row is in it. Delete forever asks first: it is the one verb here that
 * cannot be taken back. */
function TrashPanel({ trash, onClose, run, confirm }) {
  const items = trash.items ?? []
  const [sel, setSel] = useState(() => new Set())
  const anchor = useRef(null)
  const menu = useContextMenu()
  const pick = (e, id) => {
    const ids = items.map((t) => t.id)
    if (e.shiftKey && anchor.current) {
      const a = ids.indexOf(anchor.current)
      const b = ids.indexOf(id)
      setSel(new Set(ids.slice(Math.min(a, b), Math.max(a, b) + 1)))
      return
    }
    anchor.current = id
    if (e.metaKey || e.ctrlKey) setSel((prev) => { const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n })
    else setSel(new Set([id]))
  }
  const targetsFor = (id) => (sel.has(id) ? [...sel] : [id])
  const restore = (ids) => run(async () => { for (const id of ids) await trash.restore(id); setSel(new Set()) })
  const purge = async (ids) => {
    const n = ids.length
    if (confirm && !(await confirm(`Delete ${n === 1 ? 'this item' : `${n} items`} forever? This cannot be undone.`, { okLabel: 'Delete forever' }))) return
    run(async () => { for (const id of ids) await trash.purge(id); setSel(new Set()) })
  }
  return (
    <SettingsPanel variant="rail" title="Trash" onClose={onClose}
      footer={items.length > 0 && trash.empty ? (
        <SettingsFooter>
          <Button tone="primary" size="sm" onClick={async () => {
            if (confirm && !(await confirm(`Empty the trash? ${items.length} item${items.length === 1 ? '' : 's'} will be gone for good.`, { okLabel: 'Empty trash' }))) return
            run(async () => { await trash.empty(); setSel(new Set()) })
          }}>Empty trash</Button>
        </SettingsFooter>
      ) : undefined}>
      {/* THE COLUMN BROWSER'S ROWS, as they are (user 2026-09-23: *"just USE WHAT COLUMN browser
        * uses. its literally a solved problem"*) — icon, name as spelled, the same selection fill. No
        * heading, no count: it is the trash, and the files are the content. */}
      <ul className="kol-column-browser-column flex flex-col">
        {items.map((t) => {
          const name = t.path.replace(/\/$/, '').split('/').pop()
          const icon = t.isFolder ? 'folder' : ({ image: 'image', video: 'video' })[kindOf({ key: t.path })] ?? 'file'
          return (
            <li key={t.id} className={`kol-column-browser-row flex items-center gap-3 py-2 px-3 cursor-pointer transition-colors${sel.has(t.id) ? ' is-selected' : ''}`}
              onClick={(e) => pick(e, t.id)}
              onContextMenu={(e) => { if (!sel.has(t.id)) { anchor.current = t.id; setSel(new Set([t.id])) } menu.openAt(e, { ids: targetsFor(t.id) }) }}>
              <span className="w-5 shrink-0 flex items-center justify-center text-oq-48"><Icon name={icon} size={icon === 'folder' ? 18 : 14} /></span>
              <span className="kol-mono-12 min-w-0 truncate">{name}</span>
            </li>
          )
        })}
      </ul>
      <ContextMenu menu={menu}>
        {(target) => {
          if (!target) return null
          const n = target.ids.length
          return (
            <>
              {trash.restore && <MenuDropdownItem iconLeft={<Icon name="refresh" size={14} />} onClick={() => restore(target.ids)}>{n > 1 ? `Restore ${n}` : 'Restore'}</MenuDropdownItem>}
              {trash.purge && <MenuDropdownItem iconLeft={<Icon name="trash" size={14} />} onClick={() => purge(target.ids)}>{n > 1 ? `Delete ${n} forever` : 'Delete forever'}</MenuDropdownItem>}
            </>
          )
        }}
      </ContextMenu>
    </SettingsPanel>
  )
}

function LibraryHeader({ title, buckets, bucketId, appRoot, onBucket, bucketMeta, writable, headerActions, onSettings, onTrash, onHome, headerTrailing, onClose }) {
  /* the app's masthead voice when a Shell sets one (2026-09-29), else the display voice as before */
  const titleCls = mastheadTitleClass(useMasthead(), 'kol-sans-display-03')
  /* ONE BUCKET, NO DROPDOWN (user 2026-09-22) — a picker with one thing in it chooses nothing. */
  const options = buckets.length > 1 ? [{ value: 'all', label: `${title} · all` }, ...buckets.map((b) => ({ value: b.id, label: b.label }))] : []
  /* THE WORDMARK IS THE HOME BUTTON when `onHome` is given (user 2026-09-21: "it should reload,
   * like a home button"). The crumb row's root already did exactly this and the wordmark above it
   * did nothing — the one element every file manager makes clickable was the one that was inert.
   * Without the prop it stays a plain <h1>, so nothing existing changes. */
  /* CENTRED, not baseline (MediaPickerHeaderCentred, kol-monitor 2026-10-16): on the baseline the
   * controls' 12px text sat on the display title's, so every control box hung below the wordmark */
  return (
    <header className="flex items-center justify-between gap-4">
      {onHome ? (
        <button type="button" onClick={onHome}
          className={`${titleCls} text-fg-96 min-w-0 truncate text-left cursor-pointer transition-opacity hover:opacity-64`}
          aria-label={`${title} — back to the top`}>{title}</button>
      ) : (
        /* its own ink (media-picker-close-and-title, kol-mirror 2026-10-15): portalled into a body
         * with no colour, the title inherited black on the dark card */
        <h1 className={`${titleCls} text-fg-96 min-w-0 truncate`}>{title}</h1>
      )}
      <div className={`flex items-center gap-2 min-w-0${onClose ? ' ml-auto' : ''}`}>
        {options.length > 0 && <Dropdown className="min-w-0 max-w-[45vw] md:w-48 md:max-w-none" value={appRoot ? 'all' : bucketId} onChange={onBucket} options={options} />}
        {headerActions}
        {/* The DS's own `Tooltip` (utilities/Popover → `.kol-tooltip`), not a `title`
            attribute. These two were the only header chrome still handing the label to
            the browser, which drew its native box beside DS-styled ones. */}
        {!writable && bucketMeta.id && (
          <Tooltip label={`${bucketMeta.label} is read-only here`}>
            <IconFrame name="lock" variant="primary" size="sm" aria-label="Read-only" />
          </Tooltip>
        )}
        {/* THE TRASH (user 2026-09-23) — the failsafe for every delete. Search moved back to the crumb
            row, into the well beside the view switch. */}
        {onTrash && (
          <Tooltip label="Trash">
            <IconFrame name="trash" variant="primary" size="sm" onClick={onTrash} aria-label="Trash" />
          </Tooltip>
        )}
        <Tooltip label="Display settings">
          <IconFrame name="settings-01" variant="primary" size="sm" onClick={onSettings} aria-label="Display settings" />
        </Tooltip>
        {/* AFTER the icons, because the crumb row below reads `[icons] │ ROW COLUMN` and a text
          * switch in this surface sits to the RIGHT of its divider. Put before them it mirrored
          * the row directly beneath it. */}
        {headerTrailing}
      </div>
      {/* THE × IS THE CARD'S, NOT A CONTROL (media-picker-close-and-title, kol-mirror 2026-10-15 — user:
        * "you ever seen a close out button before??", pointing at the Display Settings drawer). Alone at
        * the far end of the title's row, outside the control cluster — the drawer's shape. It centres with
        * the row like every control in it. */}
      {onClose && <CloseButton onClick={onClose} className="shrink-0" />}
    </header>
  )
}

/* `columnHeight: 'fill'` — THE BROWSER TAKES WHAT IS LEFT OF THE WINDOW (media-hub, user 2026-09-26:
 * "why isnt the browser in media shell using available height like media does?"). apps/media had it
 * as a hand-measured `calc(100dvh - 212px)` — its header, gaps and padding, counted once — so the same
 * tool one shell over got the 528 base. Measured instead: the window, less the view's own top, less
 * what the page draws under it (the count line), less the bottom padding / border / margin of every
 * box around it. Follows a resize and any change above it. Floor 240 so a short window still browses. */
/* `view` is the ELEMENT (state, via a callback ref), not a ref object: the view mounts after the data
 * loads, and in a fixed-height page (kol-shell PageShell) nothing else resizes to wake the observer —
 * a ref read once at mount stayed null and the browser sat at the 528 base. */
function useFillHeight(on, view, pageEl) {
  const [h, setH] = useState(null)
  useLayoutEffect(() => {
    if (!on || !view) return undefined
    const measure = () => {
      const page = pageEl.current
      if (!page) return
      const r = view.getBoundingClientRect()
      /* what the page DRAWS under the view — its content's bottom, not its box's: a page stretched to
       * fill its container (flex-1) would count its own empty space as "below" and freeze the height */
      const contentBottom = Math.max(r.bottom, ...[...page.children].map((c) => c.getBoundingClientRect().bottom))
      let below = contentBottom - r.bottom
      for (let el = page; el && el !== document.body; el = el.parentElement) {
        const cs = getComputedStyle(el)
        below += parseFloat(cs.paddingBottom) + parseFloat(cs.borderBottomWidth) + (el === page ? 0 : parseFloat(cs.marginBottom))
      }
      const next = Math.max(240, Math.floor(window.innerHeight - r.top - below))
      setH((p) => (p === next ? p : next))
    }
    measure()
    const ro = new ResizeObserver(measure)
    ro.observe(document.body)
    if (pageEl.current) ro.observe(pageEl.current)
    window.addEventListener('resize', measure)
    return () => { ro.disconnect(); window.removeEventListener('resize', measure) }
  }, [on, view]) // eslint-disable-line react-hooks/exhaustive-deps
  return h
}

/* the profile — what the bucket holds; drives the chips and the settings' "nothing to fold" states */
const profileOf = (objects, rawFiles, systemCount) => ({
  variantSets: groupVariants(objects.map((o) => ({ ...o, displayKey: o.key }))).filter((g) => g.variants).length,
  segments: objects.filter((o) => isSegment(o.key)).length,
  kinds: { ...rawFiles.reduce((acc, o) => { acc[o.kind] = (acc[o.kind] || 0) + 1; return acc }, {}), ...(systemCount ? { system: systemCount } : {}) },
})

/* ══ BROWSE — folder / files ═══════════════════════════════════════════════ */
/* THE `···`'s SORT, pure and module-level so it is reachable by a check
 * (ColumnBrowserMobileViews item 15). Sorting was desktop-only chrome until
 * now — item 7 of the previous ticket hid the control cluster below `md` and
 * put nothing in its place, so on a phone sort could not be reached at all.
 *
 * NAME IS THE TIEBREAK IN EVERY MODE. Two files of equal size, or a bucket
 * whose objects carry no `uploaded`, otherwise come back in whatever order the
 * comparator happened to walk — a list that reshuffles between renders for no
 * reason the user did anything to cause. The tiebreak is deliberately NOT
 * reversed by `sortDir`: descending by size still reads A before B inside a
 * tie, which is what every file manager does. */
function sortObjects(objects, sortBy = 'name', sortDir = 'asc') {
  const nameOf = (o) => o.key.slice(o.key.lastIndexOf('/') + 1).toLowerCase()
  const val = {
    name: nameOf,
    date: (o) => o.uploaded ?? '',
    size: (o) => o.size ?? 0,
    kind: (o) => String(kindOf(o)),
  }[sortBy] ?? nameOf
  const dir = sortDir === 'desc' ? -1 : 1
  return [...objects].sort((m, n) => {
    const x = val(m), y = val(n)
    if (x === y) return nameOf(m) < nameOf(n) ? -1 : 1
    return (x < y ? -1 : 1) * dir
  })
}

export function MediaLibraryBrowse({
  client, title = 'MEDIA', bucket, onBucketChange, prefix: prefixProp, onPrefix, defaults, settings: settingsProp, onSettingsChange,
  /* THE ONE VIEW (2026-09-22): 'columns' | 'rows' | 'grid' | 'list'. Controlled or from settings;
   * `folderView` and `layout` are kept in step beneath it so a consumer still on those keeps
   * rendering what it rendered. */
  view: viewProp, onViewChange,
  folderTree, headerActions, headerTrailing, refreshKey, onOpen, autoFocus = false, settingsFooter, className = '', banner,
  /* `onHeaderClose` — a × alone at the far end of the title row, outside the controls (the picker card) */
  onHeaderClose,
  /* `onInsert(o)` — a file row's menu LEADS with Insert (the picker on a phone, user 2026-10-23: *"you just select the
   * image and maybe dropdown says insert"*); `canInsert(o)` gates it per file (the picker's `accept`) */
  onInsert,
  canInsert,
  /* `smartFolder` — a smart folder's id to open ON (a Home tile linking straight to one); the chips
   * take over from there (media D1 plan v2) */
  smartFolder,
  /* THE FILE VERBS, as ONE seam (2026-09-21). `{ createFolder, rename, move, remove }`, each
   * optional and each async; whatever is supplied becomes a right-click menu entry and
   * anything absent simply is not offered. One object rather than four props because they
   * arrive together — a consumer with a writable store has all of them, one with a read-only
   * bucket has none — and because the page must never have to know WHICH verbs exist to
   * decide whether to draw a menu at all. The consumer re-lists after a mutation by bumping
   * `refreshKey`; the page does not own the data.
   *
   * `fileActions.items` — THE CONSUMER'S OWN VERBS (kol-client-olina 2026-09-22: Duplicate was
   * a DS publish away, and so would every verb after it be). `[{ label, icon?, when?, run }]`;
   * each gets the menu's payload `{ type, path, o?, targets? }`, `when` filters per target
   * (absent = always), and `run` goes through the same busy/error path as the built-ins. They
   * render AFTER the built-in verbs. Without `items` the menu is exactly what it was. */
  fileActions,
  /* `onDropFiles(files, folderPath)` — desktop files dropped on a folder row or a level
   * (kol-client-olina 2026-09-22). The page hands over the `FileList` and the bucket-relative
   * folder and NEVER uploads: every consumer's pipeline differs. The consumer re-lists through
   * `refreshKey`, the same contract as `fileActions`. Writable buckets only; absent, nothing. */
  onDropFiles,
  /* `trash` — THE FAILSAFE (user 2026-09-23). `{ items, restore(id), purge(id), empty() }`, the
   * consumer's: `items` is `[{ id, path, deletedAt, isFolder?, count?, size?, expiresAt? }]`, and
   * `fileActions.remove` is expected to move into it. Given, delete stops asking "this cannot be
   * undone" — it can — and a trash button opens the list with Restore and Delete forever. The
   * consumer re-lists through `refreshKey`. Absent, delete is what it was. */
  trash,
  /* PASS-THROUGHS TO `ColumnBrowser`. A documented prop this page does not
   * forward is a prop no consumer of the PAGE can reach, which makes the seam
   * fictional — `thumbnailFor` and `folderMeta` shipped, were announced, and
   * rendered as if they never existed because this signature ended before them
   * (ColumnBrowserMobileViews items 10 + 13, kol-r2b2 2026-09-04). Second time
   * this shape bit: `SettingsPanel` documented a `footer` slot this file
   * hardcoded past. The rule the sweep leaves behind — WHEN A PROP IS ADDED TO
   * `ColumnBrowser`, IT IS ADDED HERE IN THE SAME EDIT, unless the page holds a
   * real opinion about it (it owns `height`, the widths and `partition`, so
   * those are deliberately absent from this list). */
  thumbnailFor, folderMeta, stackView,
  /* defaulted, not bare — see the note on the sibling page */
  formatDate = defaultFormatDate,
  /* THE TAB PILL'S LIST (ColumnBrowserMobileViews item 16). What a tab MEANS is
   * the consumer's — a repo whose surfaces are routes wires its router here.
   * No tabs, no pill, and the page is exactly what it was. */
  tabs, activeTab, onTabChange,
  /* `onKinds` — opens the app's file-formats overview. Given, the phone's `···` menu carries it
   * under view and sort (apps review 2026-09-29: the Files app keeps everything in its `…`, and
   * the Kinds tab was the only reason the phone had a tab bar). */
  onKinds,
  /* `onOpenSettings` — the gear HANDS OVER to the host (apps review 2026-09-29: three settings in
   * media-hub, "two too many"). Given, the gear calls it and this page opens no drawer of its own —
   * inside a Hub the Hub's Settings is the one place. Absent, the gear opens the drawer as always. */
  onOpenSettings,
  /* `searchSuggestions` — what the search modal lists before you type. Absent: favourites, smart
   * folders, then the bucket's top-level folders. An array replaces them; a function receives them
   * and returns the list. Rows are the search modal's shape; a row with `run` calls it instead of
   * jumping to `id` as a file key. */
  searchSuggestions,
  /* `startAtRoot` — open at the title root, the buckets listed, instead of inside `bucket` (the picker, 2026-10-23) */
  startAtRoot = false,
  /* `bucketLevel` — keep title → bucket → folders with ONE bucket (kol-client-olina 2026-09-23).
   * Absent, a one-bucket consumer collapses the level as ruled 2026-09-03. */
  bucketLevel = false,
  /* `onPickFile(o | null)` — the selected file, reported as it changes (picker A, 2026-10-08: the
   * browse surface inside the modal card needs the selection for its Use button). Keys are
   * bucket-relative. Absent, nothing. */
  onPickFile,
  /* `searchPlaceholder` — the search field's words; a store that is not a bucket names itself (kol-fxr 2026-10-08) */
  searchPlaceholder = 'Search this bucket',
  /* `o.displayName` on a listed object is its LABEL in every view, Quick Look and search (kol-fxr
   * 2026-10-08: names not unique, so the key is an id). Absent, the label is the key, as before. */
}) {
  const [ownPrefix, setOwnPrefix] = useState('')
  const prefix = prefixProp ?? ownPrefix
  const setPrefix = (v) => { if (v) setAppRoot(false); if (prefixProp == null) setOwnPrefix(v); onPrefix?.(v) }
  const [appRoot, setAppRoot] = useState(!!startAtRoot)
  const [ownBucket, setOwnBucket] = useState(bucket)
  const bucketId = bucket ?? ownBucket
  const lib = useBucketLibrary({ client, bucket: bucketId, defaults, settings: settingsProp, onSettingsChange, refreshKey })
  const { buckets, bucketMeta, settings, setSettings, objects, error, mediaUrl, downloadUrl, hasUrl } = lib
  /* `fileActions` GIVEN IS A WRITER (kol-fxr 2026-10-08): the consumer brings its own verbs, so the
   * client need not carry a `deleteObject` stub to be treated as writable. The bucket still has to
   * say it is writable. */
  const writable = lib.writable || (!!fileActions && !!bucketMeta.writable)
  const switchBucket = (id, pfx = '') => { if (bucket == null) setOwnBucket(id); onBucketChange?.(id); setPrefix(pfx) }
  const [settingsOpen, setSettingsOpen] = useState(false)
  const [trashOpen, setTrashOpen] = useState(false)
  const [pickedFile, setPickedFile] = useState(null)
  useEffect(() => { onPickFile?.(pickedFile) }, [pickedFile]) // eslint-disable-line react-hooks/exhaustive-deps
  /* ROW VIEW PICKS, and only one at a time: a click selects and fills the preview, a DOUBLE click
   * opens (user 2026-09-22, Finder's behaviour — "clicking shouldnt automatically open it"). The
   * chevron still expands in place, which is the third thing and always was. */
  const [pickedFolder, setPickedFolder] = useState(null)
  const pickFile = (o) => { setAppRoot(false); setPickedFolder(null); setPickedFile(o) }
  const pickFolder = (path) => { setAppRoot(false); setPickedFile(null); setPickedFolder(path) }
  /* AT THE TITLE ROOT IS DERIVED, NOT STORED (kol-client-olina 2026-09-23: rows and grid picked a
   * file under `brand/logos/` and the crumb still read the title alone). `appRoot` was a second
   * source of truth for the location, and only the columns ever cleared it. A path or a pick lives
   * inside a bucket, so either one means you are not at the title — a stale flag can no longer
   * hide where you are. Everything below reads this, never `appRoot`. */
  const atTitleRoot = appRoot && !prefix && !pickedFile && !pickedFolder
  /* ABOVE THE TITLE (user 2026-09-24: *"this is not the top root folder … in column view I can see
   * the root MEDIA"*). Column 0 is a level whose one item is the title; rows and grid had no such
   * level, so the bucket list was their top and the title never appeared as a thing. `appRoot` is
   * `'top'` there — the same state, one level higher, derived the same way. */
  const atTop = atTitleRoot && appRoot === 'top'
  /* a BUCKET is a folder with another icon: a click highlights it, a double-click opens it */
  const [pickedBucket, setPickedBucket] = useState(null)
  const ROOT_KEY = '\u0000title'
  /* IN THE ROWS, WHAT YOU PICK SETS WHERE YOU ARE (user 2026-09-23: *"that files folder becomes the
   * default NOT the previous folder"*). The list is rooted at the top, so a pick can sit in any open
   * folder; the current folder follows it to the pick's parent. Whatever the old path had open
   * STAYS open — moving where you are must not fold up what you were looking at. */
  const dirOf = (path) => { const t = path.replace(/\/$/, ''); return t.slice(0, t.lastIndexOf('/') + 1) }
  const moveHereKeepingOpen = (to) => {
    if (to === prefix) return
    setExpandedRows((prev) => new Set([...prev, ...openChain(prefix)]))
    setPrefix(to)
  }
  const rowPickFile = (o) => { pickFile(o); moveHereKeepingOpen(dirOf(o.key)) }
  const rowPickFolder = (path) => { pickFolder(path); moveHereKeepingOpen(dirOf(path)) }
  /* Row view's inline disclosure set — folder paths currently expanded. Kept here rather than in
   * settings: it is a transient reading state, not a preference, and Finder does not persist it
   * across a relaunch either. */
  const [expandedRows, setExpandedRows] = useState(() => new Set())
  /* THE ROW LIST IS ALWAYS BASED AT THE ROOT (user 2026-09-23: *"row view should not open a path
   * of the folder its highlighted … rather expand the folders to that location, still showing the
   * root"*). It used to re-base on `prefix`, so arriving from the columns three folders deep threw
   * the tree away and showed one level. The path now seeds the EXPANSION instead: every ancestor
   * opens, the folder you are in reads selected, and the root is still the first row.
   *
   * Collapsing a folder ON that path is how you leave it — the chevron cannot fight `prefix` and
   * win, so it moves `prefix` up instead of flickering back open. */
  const openChain = (p) => {
    const out = []
    let cur = ''
    for (const seg of p.replace(/\/$/, '').split('/').filter(Boolean)) { cur += `${seg}/`; out.push(cur) }
    return out
  }
  const rowsExpanded = new Set([...expandedRows, ...openChain(prefix)])
  /* THE VISIBLE ORDER, flattened in render order, so ⇧-click ranges across expanded folders and
   * not just the level you happen to be on. Built from the same `rowLevel` + expansion the list
   * draws from, so it cannot disagree with what is on screen. */
  const visibleRowOrder = (list) => {
    const out = []
    const walk = (level) => {
      const { folders, files } = rowLevel(level, list)
      for (const f of folders) {
        const path = level + f
        out.push(path)
        if (rowsExpanded.has(path)) walk(path)
      }
      for (const o of files) out.push(o.key)
    }
    walk('')
    return out
  }
  const toggleRow = (path) => {
    if (openChain(prefix).includes(path)) { setPrefix(path.replace(/[^/]+\/$/, '')); return }
    setExpandedRows((prev) => {
      const next = new Set(prev)
      next.has(path) ? next.delete(path) : next.add(path)
      return next
    })
  }

  /* ── THE FILE VERBS ────────────────────────────────────────────────────────────────────────
   * Right-click is the surface; `fileActions` is the implementation. The page owns neither the
   * data nor the store — it collects an intent, asks for the one piece of text it needs, calls
   * the consumer's verb and lets the consumer re-list. The asking goes through `useModal()` —
   * the DS's one dialog idiom — so a consumer that mounts `ModalProvider` gets DS dialogs and one
   * that does not gets the native ones, exactly as before (kol-client-olina 2026-09-22: the
   * browser's own grey prompt box was the last non-DS surface on media.olina-productions.com). */
  const menu = useContextMenu()
  /* touch has no right button: a held press on a row or tile opens the same menu (useLongPress) */
  const press = useLongPress()
  const modal = useModal()
  const canWrite = !!fileActions && writable
  /* ── WHAT THE DATABASE BESIDE THE BUCKET ADDS (media D1 plan v2, 2026-09-26) ──────────────────
   * Every verb optional; each feature below is off when its verb is missing. Writes patch the list
   * in place so the change lands at once; the next listing confirms it. A path ending in `/` is a
   * folder — its rows come from `folderInfo`, a file's ride the listing. */
  const bucketKey = bucketMeta.id ?? undefined
  const folderRows = lib.folderInfo
  const patchPath = (path, patch) => {
    if (path.endsWith('/')) lib.setFolderInfo((f) => ({ ...f, [path]: { tags: [], favourite: false, ...(f[path] ?? {}), ...patch } }))
    else {
      lib.setObjects((list) => list.map((o) => (o.key === path ? { ...o, ...patch } : o)))
      setPickedFile((f) => (f?.key === path ? { ...f, ...patch } : f))
    }
  }
  /* TAGS — `setTags(path, tags, bucket)` */
  const canTag = writable && typeof client?.setTags === 'function'
  const tagsOf = (path) => (path.endsWith('/') ? folderRows[path]?.tags : objects.find((o) => o.key === path)?.tags) ?? []
  /* every tag in the bucket, for the add field's suggestions — a typo forks the vocabulary */
  /* the rows view grows a Tags column when there is anything to show or set */
  const hasTags = canTag || objects.some((o) => o.tags?.length)
  const allTags = useMemo(() => [...new Set([...objects.flatMap((o) => o.tags ?? []), ...Object.values(folderRows).flatMap((f) => f.tags ?? [])])].sort(), [objects, folderRows])
  const saveTags = (path, tags) => runAction(async () => {
    const r = await client.setTags(path, tags, bucketKey)
    patchPath(path, { tags: r?.tags ?? tags })
  })
  /* FAVOURITES — `setFavourite(path, on, bucket)` */
  const canFavourite = writable && typeof client?.setFavourite === 'function'
  const isFavourite = (path) => !!(path.endsWith('/') ? folderRows[path]?.favourite : objects.find((o) => o.key === path)?.favourite)
  const toggleFavourite = (path) => runAction(async () => {
    const on = !isFavourite(path)
    await client.setFavourite(path, on, bucketKey)
    patchPath(path, { favourite: on })
  })
  /* THE EVENT LOG recents are read from — intentions only (opened, edited, created) */
  const logEvent = (kind, path) => { Promise.resolve(client?.logEvent?.(kind, path, bucketKey)).catch(() => {}) }
  /* DRAFTS are browser memory (the user's ruling) — read here only to mark a file that has one */
  const [draftKeys, setDraftKeys] = useState(() => new Set(listDrafts(bucketKey).map((d) => d.key)))
  useEffect(() => {
    const read = () => setDraftKeys(new Set(listDrafts(bucketKey).map((d) => d.key)))
    read()
    window.addEventListener(DRAFTS_EVENT, read)
    return () => window.removeEventListener(DRAFTS_EVENT, read)
  }, [bucketKey])
  /* DOCUMENTS — `readText` / `writeText` edit; a new one is the consumer's `createFile` + `writeText` */
  const canEdit = writable && typeof client?.readText === 'function' && typeof client?.writeText === 'function'
  const canNewDoc = canEdit && canWrite && !!fileActions.createFile
  const [editing, setEditing] = useState(null) // { mode: 'edit', o } | { mode: 'new', folder }
  const openEditor = (o) => { setEditing({ mode: 'edit', o }); logEvent('opened', o.key) }
  const newDocument = (folder) => setEditing({ mode: 'new', folder })
  /* A MARKDOWN FILE'S FRONTMATTER TAGS ARE ITS TAGS — merged into the database's on every save, so
   * a tag written in the document is a tag you can filter by. Merged, never replaced: a tag set in
   * the pane and not in the document stays. */
  const syncFrontmatterTags = async (key, text) => {
    if (!canTag || !/\.(md|markdown)$/i.test(key)) return
    const fm = parseFrontmatter(text ?? '').tags
    const typed = (Array.isArray(fm) ? fm : typeof fm === 'string' && fm ? fm.split(',') : []).map((t) => String(t).trim().toLowerCase()).filter(Boolean)
    const current = objects.find((o) => o.key === key)?.tags ?? []
    const next = [...new Set([...current, ...typed])]
    if (next.length !== current.length) { await client.setTags(key, next, bucketKey); patchPath(key, { tags: next }) }
  }
  const afterEdit = (key, res, text) => {
    if (res?.size != null) lib.setObjects((list) => list.map((o) => (o.key === key ? { ...o, size: res.size, uploaded: new Date().toISOString() } : o)))
    syncFrontmatterTags(key, text).catch(() => {})
  }
  const createDocument = async ({ name, text }) => {
    const key = `${editing.folder}${name}`
    if (objects.some((o) => o.key === key)) throw new Error(`${name} already exists here`)
    await fileActions.createFile(key)
    await client.writeText(key, text, bucketKey)
    const fm = /\.(md|markdown)$/i.test(key) ? parseFrontmatter(text).tags : null
    const typed = (Array.isArray(fm) ? fm : []).map((t) => String(t).trim().toLowerCase()).filter(Boolean)
    if (canTag && typed.length) await client.setTags(key, typed, bucketKey)
    lib.reload()
    setEditing(null)
  }
  /* what a markdown document can attach: the bucket's own files, linked by their public URL */
  const attachables = editing ? objects.filter((o) => !isSystemFile(o.key)).map((o) => ({ key: o.key, name: o.key.split('/').pop(), url: mediaUrl(o.key), contentType: o.contentType })) : []
  const editBlock = (o) => {
    const kind = editKindOf(o)
    if (!canEdit || !kind) return null
    const tooBig = (o.size ?? 0) > EDIT_CAP
    return (
      <div className="flex items-center gap-3">
        <Button size="sm" iconLeft="edit" disabled={tooBig} onClick={() => openEditor(o)}>Edit</Button>
        {tooBig && <span className="kol-mono-12 text-fg-48">Over 1 MB — too large to edit here</span>}
        {!tooBig && draftKeys.has(o.key) && <span className="kol-mono-12 text-fg-48">Unsaved draft</span>}
      </div>
    )
  }
  /* what the preview pane adds under a file's or a folder's facts, in every view */
  const detailsFor = (o) => {
    const path = o.key ?? o
    const tags = tagsOf(path)
    const edit = typeof o === 'object' ? editBlock(o) : null
    if (!edit && !canTag && !canFavourite && !tags.length) return null
    return (
      <div className="flex flex-col gap-4">
        {(edit || canFavourite) && (
          <div className="flex items-center gap-3">
            {canFavourite && (
              <Tooltip label={isFavourite(path) ? 'Remove from favourites' : 'Add to favourites'}>
                <Button variant="nav" size="sm" iconOnly={isFavourite(path) ? 'star-solid' : 'star'} onClick={() => toggleFavourite(path)}
                  aria-label={isFavourite(path) ? 'Remove from favourites' : 'Add to favourites'} aria-pressed={isFavourite(path)} />
              </Tooltip>
            )}
            {edit}
          </div>
        )}
        {(canTag || tags.length > 0) && <TagEditor tags={tags} suggestions={allTags} onChange={canTag ? (next) => saveTags(path, next) : undefined} />}
      </div>
    )
  }
  const doTags = async (keys) => {
    const one = keys.length === 1
    const current = one ? tagsOf(keys[0]).join(', ') : ''
    const answer = await modal.prompt(one ? 'Tags, separated by commas:' : `Add tags to ${keys.length} items, separated by commas:`, current, { okLabel: 'Save' })
    if (answer == null) return
    const typed = answer.split(',').map((t) => t.trim().toLowerCase()).filter(Boolean)
    for (const key of keys) await saveTags(key, one ? typed : [...new Set([...tagsOf(key), ...typed])])
  }
  /* SMART FOLDERS — `smartFolders(bucket)` · `saveSmartFolder(sf, bucket)` · `deleteSmartFolder(id)` */
  const canSmart = typeof client?.smartFolders === 'function'
  const [smartFolders, setSmartFolders] = useState([])
  const [activeSmart, setActiveSmart] = useState(null)
  const loadSmart = () => { if (canSmart) Promise.resolve(client.smartFolders(bucketKey)).then((l) => setSmartFolders(l ?? [])).catch(() => {}) }
  useEffect(() => { setActiveSmart(null); loadSmart() }, [bucketKey]) // eslint-disable-line react-hooks/exhaustive-deps
  useEffect(() => { if (smartFolder && smartFolders.some((f) => f.id === smartFolder)) setActiveSmart(smartFolder) }, [smartFolder, smartFolders])
  const activeSmartFolder = smartFolders.find((f) => f.id === activeSmart) ?? null
  const saveSmart = async () => {
    const name = await modal.prompt('Smart folder name:', '', { okLabel: 'Next' })
    if (!name?.trim()) return
    const tags = await modal.prompt('Tags every file must carry, separated by commas (blank for any):', '', { okLabel: 'Next' })
    if (tags == null) return
    const kinds = await modal.prompt('Kinds, separated by commas — image, video, audio, markdown, text, json, pdf… (blank for all):', '', { okLabel: 'Save' })
    if (kinds == null) return
    const split = (v) => v.split(',').map((t) => t.trim().toLowerCase()).filter(Boolean)
    runAction(async () => {
      const row = await client.saveSmartFolder({ name: name.trim(), query: { tags: split(tags), kinds: split(kinds), text: '' } }, bucketKey)
      loadSmart()
      if (row?.id) setActiveSmart(row.id)
    })
  }
  const deleteSmart = async (sf) => {
    if (!(await modal.confirm(`Delete the smart folder "${sf.name}"? The files stay where they are.`, { okLabel: 'Delete' }))) return
    runAction(async () => { await client.deleteSmartFolder(sf.id, bucketKey); if (activeSmart === sf.id) setActiveSmart(null); loadSmart() })
  }
  const [busyAction, setBusyAction] = useState(false)

  const runAction = async (fn) => {
    if (!fn) return
    setBusyAction(true)
    try { await fn() } catch (e) { await modal.alert(e.message) } finally { setBusyAction(false) }
  }

  const doCreateFolder = async (parent) => {
    const name = await modal.prompt(`New folder in ${parent || 'the bucket root'}:`, '', { okLabel: 'Create' })
    if (!name?.trim()) return
    runAction(() => fileActions.createFolder(`${parent}${name.trim().replace(/^\/+|\/+$/g, '')}/`))
  }
  const doCreateFile = async (parent) => {
    const name = await modal.prompt(`New file in ${parent || 'the bucket root'}:`, 'untitled.txt', { okLabel: 'Create' })
    if (!name?.trim()) return
    runAction(() => fileActions.createFile(`${parent}${name.trim().replace(/^\/+/, '')}`))
  }
  const doRename = async (path, isFolder) => {
    const current = path.replace(/\/$/, '').split('/').pop()
    const name = await modal.prompt(`Rename ${isFolder ? 'folder' : 'file'}:`, current, { okLabel: 'Rename' })
    if (!name?.trim() || name === current) return
    const parent = path.replace(/\/$/, '').slice(0, path.replace(/\/$/, '').length - current.length)
    const to = `${parent}${name.trim()}${isFolder ? '/' : ''}`
    /* a draft is keyed by path — it follows the file (or everything under the folder) */
    runAction(async () => { await fileActions.rename(path, to); moveDrafts(bucketKey, path, to) })
  }
  const doMove = async (path) => {
    const dest = await modal.prompt(`Move into which folder? (blank = bucket root)`, prefix, { okLabel: 'Move' })
    if (dest == null) return
    const clean = dest ? `${dest.replace(/^\/+|\/+$/g, '')}/` : ''
    runAction(async () => { await fileActions.move(path, clean); afterMove([path], clean) })
  }
  /* A MOVE KEEPS WHAT YOU MOVED IN HAND (user 2026-09-23: *"they should remain selected and new
   * location shown"*). The set is re-keyed to where the items now live, and the target comes into
   * view: expanded in place in the rows, navigated into in the columns and the grid. */
  const afterMove = (paths, dest) => {
    const moved = paths.map((p) => `${dest}${p.replace(/\/$/, '').split('/').pop()}${p.endsWith('/') ? '/' : ''}`)
    paths.forEach((p, i) => moveDrafts(bucketKey, p, moved[i]))
    setPickedFile(null); setPickedFolder(null)
    setRowSelection(new Set(moved))
    selectAnchorRef.current = moved[0] ?? null
    if (view === 'rows') setExpandedRows((prev) => new Set([...prev, ...openChain(dest)]))
    else setPrefix(dest)
  }
  /* DRAG TO MOVE. HTML5 drag-and-drop, not a pointer-tracking implementation: the rows are a list,
   * the targets are folders, and the browser already does the hit-testing and the cursor. The
   * dragged path lives in a ref rather than dataTransfer alone so `canDrop` can answer without
   * reading the event — a folder may not be dropped into itself or into its own subtree, which is
   * the one rule that makes this safe to wire straight to `move`. */
  /* MULTI-SELECT in the browse views. ⌘/Ctrl-click adds one, ⇧-click takes the run between the
   * anchor and the click, a plain click clears back to navigation. The wall has had its own SELECT
   * mode since 0.119.0; this is the browse half, and it deliberately does NOT reuse that mode's
   * chrome — there the wall IS the surface and a mode bar fits, here selection is a modifier on a
   * tree you are still walking. The menu acts on the whole set when the clicked row is in it. */
  const [rowSelection, setRowSelection] = useState(() => new Set())
  const selectAnchorRef = useRef(null)
  /* where the keyboard IS, apart from where a ⇧ run began — null means "at the anchor" */
  const selectCursorRef = useRef(null)
  /* DRAG A BAND ACROSS THE LIST (user 2026-09-22). The same set the ⌘/⇧ clicks build, so the menu
   * acts on a dragged selection exactly as it does on a clicked one. */
  const marqueeSelect = (keys, additive) => setRowSelection((prev) => (additive ? new Set([...prev, ...keys]) : new Set(keys)))
  const rowsMarquee = useMarquee({ onSelect: marqueeSelect })
  const gridMarquee = useMarquee({ onSelect: marqueeSelect })

  const selectRow = (path, e, orderedPaths) => {
    selectCursorRef.current = null
    const additive = e.metaKey || e.ctrlKey
    const ranged = e.shiftKey && selectAnchorRef.current
    /* A PLAIN CLICK IS A SELECTION OF ONE (user 2026-09-23). It used to CLEAR the set and only
     * "pick" the row for the preview, so a row could look selected while the set was empty — and
     * a drag, which carries the set, then took one fewer item than the screen showed. The pick and
     * the selection are the same act; `false` still tells the caller to preview it. */
    if (!additive && !ranged) { setRowSelection(new Set([path])); selectAnchorRef.current = path; return false }
    e.preventDefault()
    setRowSelection((prev) => {
      const next = new Set(prev)
      if (ranged) {
        const a = orderedPaths.indexOf(selectAnchorRef.current)
        const b = orderedPaths.indexOf(path)
        if (a !== -1 && b !== -1) for (let i = Math.min(a, b); i <= Math.max(a, b); i++) next.add(orderedPaths[i])
      } else {
        next.has(path) ? next.delete(path) : next.add(path)
        selectAnchorRef.current = path
      }
      return next
    })
    return true
  }

  /* The set the menu acts on: the selection when you right-clicked something inside it, otherwise
   * just the row you hit — which is what every file manager does and what stops a right-click on an
   * unrelated row from silently deleting five things. */
  const targetsFor = (path) => (rowSelection.has(path) ? [...rowSelection] : [path])

  const doBatch = async (paths, verb, label) => {
    if (paths.length === 1) return null
    if (label === 'Delete' && !trash && !(await modal.confirm(`Delete ${paths.length} items? This cannot be undone.`, { okLabel: 'Delete' }))) return null
    let dest = null
    if (label === 'Move') {
      dest = await modal.prompt(`Move ${paths.length} items into which folder? (blank = bucket root)`, prefix, { okLabel: 'Move' })
      if (dest == null) return null
      dest = dest ? `${dest.replace(/^\/+|\/+$/g, '')}/` : ''
    }
    runAction(async () => {
      for (const p of paths) {
        if (label === 'Delete') await fileActions.remove(p)
        else await fileActions.move(p, dest)
      }
      if (label === 'Delete') setRowSelection(new Set())
      else afterMove(paths, dest)
    })
    void verb
    return true
  }

  /* THE DRAG CARRIES THE SELECTION (user 2026-09-23: "if 3 are selected, drag and drop still only
   * grabs one"). Dragging a row that is IN the selection drags the whole set — which is what the
   * right-click menu already did, and the two must not disagree about what "the targets" means.
   * Dragging a row outside the selection is that row alone, as every file manager does. */
  const draggingRef = useRef(null)
  const dragFor = canWrite && fileActions.move ? (path) => ({
    path,
    onDragStart: (e, from) => {
      const set = rowSelection.has(from) ? [...rowSelection] : [from]
      draggingRef.current = set
      e.dataTransfer.effectAllowed = 'move'
      e.dataTransfer.setData('text/plain', set.join('\n'))
    },
    /* every dragged path has to be legal, or the drop would half-happen */
    canDrop: (dest) => {
      const set = draggingRef.current
      if (!set?.length) return false
      /* ONLY A FOLDER TAKES A DROP. A file row is not a destination: moving `a.jpg` "into"
       * `b.json` renamed it to `b.jsona.jpg`-shaped keys (the file-onto-file bug, 2026-09-22). */
      if (dest && !dest.endsWith('/')) return false
      return set.every((from) => {
        if (from === dest) return false
        if (from.endsWith('/') && dest.startsWith(from)) return false // into itself
        const parent = from.replace(/\/$/, '').split('/').slice(0, -1).join('/')
        return dest !== (parent ? `${parent}/` : '')                  // already there
      })
    },
    onDrop: (dest) => {
      const set = draggingRef.current
      draggingRef.current = null
      if (!set?.length) return
      runAction(async () => {
        for (const from of set) await fileActions.move(from, dest)
        afterMove(set, dest)
      })
    },
  }) : null

  /* the OS-drop target for a bucket-relative folder path, or null where nothing may land */
  const dropFilesTo = onDropFiles && writable ? (path) => (files) => onDropFiles(files, path) : null

  /* UPLOAD FROM THE MENU (user 2026-09-22). The drop zone asked where files should go and never
   * said; right-click names the folder. The picker is the browser's own and the files go through
   * `onDropFiles`, the same seam a drag uses — the page still never uploads. */
  const doUpload = (path) => {
    const input = document.createElement('input')
    input.type = 'file'
    input.multiple = true
    input.onchange = () => { if (input.files?.length) runAction(() => onDropFiles(input.files, path)) }
    input.click()
  }

  /* The wall's two read verbs, in the tree's menu too (user 2026-09-22: "in right click dropdown we
   * need copy url and download options, they are currently only availble in files mode"). Both are
   * the client's URLs, so neither needs a write seam — they are offered wherever a FILE is. */
  const doCopyUrl = (paths) => navigator.clipboard?.writeText(paths.map((p) => mediaUrl(p)).join('\n'))
  const doDownload = (paths) => {
    for (const p of paths) {
      const a = document.createElement('a')
      a.href = downloadUrl(p)
      a.download = p.split('/').pop()
      document.body.appendChild(a)
      a.click()
      a.remove()
    }
  }

  const doDelete = async (path, isFolder) => {
    if (!trash && !(await modal.confirm(`Delete ${isFolder ? `"${path}" and everything in it` : `"${path}"`}? This cannot be undone.`, { okLabel: 'Delete' }))) return
    runAction(() => fileActions.remove(path))
  }
  /* stepping into a folder drops the pick — the preview would otherwise describe something no
   * longer in the list */
  /* GOING UP SELECTS THE FOLDER YOU CAME FROM (user 2026-09-24: back from `01-shoots` landed on
   * `img/` with nothing selected and an empty preview — every file manager lights the child you
   * left). One rule for every way up — ⌘↑ and the crumbs both come through here. */
  const goFolder = (path) => {
    const child = path !== prefix && prefix.startsWith(path) ? `${path}${prefix.slice(path.length).split('/')[0]}/` : null
    setPickedFile(null); setPickedFolder(null); setPrefix(path)
    if (child) { setRowSelection(new Set([child])); setPickedFolder(child); selectAnchorRef.current = child; selectCursorRef.current = null }
  }
  /* ONE "GO UP" FOR ALL THREE VIEWS (kol-client-olina 2026-09-23: *"3 different stops for 3
   * modes … they should all go all the way to the shared root folder"*): parent folder → … → the
   * bucket's top → the title root, which is what `onHome` does. With one bucket and no
   * `bucketLevel` there is no level above the bucket's top, so that is where it ends. Columns walk
   * the same path with ArrowLeft. */
  const goUp = () => {
    if (prefix) { goFolder(dirOf(prefix)); return }
    if (!single && !atTitleRoot) goTitleRoot()
    else if (!single && !atTop) goTop()
  }
  /* THE TITLE ROOT CLEARS EVERYTHING that would say you are somewhere else — a pick, a selection —
   * or the derivation above reads you as still inside the bucket */
  const goTop = () => { setAppRoot('top'); setPickedFile(null); setPickedFolder(null); setRowSelection(new Set()); setPickedBucket(ROOT_KEY); setPrefix('') }
  const goTitleRoot = () => { setAppRoot(true); setPickedFile(null); setPickedFolder(null); setPickedBucket(atTitleRoot ? null : (bucketMeta.id ?? bucketMeta.label)); setRowSelection(new Set()); setPrefix('') }
  /* opening a bucket from the title root, the step the columns' column 1 is */
  const openBucket = (b) => {
    setAppRoot(false); setPickedFile(null); setPickedFolder(null); setPickedBucket(null); setRowSelection(new Set())
    if (b.id != null && b.id !== bucketMeta.id) switchBucket(b.id, ''); else setPrefix('')
  }
  /* SPACE IS QUICK LOOK IN EVERY VIEW (user 2026-09-23: *"spacebar preview only works in column,
   * fix that. CONSISTENCY"*). `ColumnBrowser` has owned that key since it shipped, so the columns
   * had it and the two views built later did not. The target is whatever is picked — the previewed
   * file, or a selection of exactly one — and the set it pages through is the list you are looking
   * at: the folder's files in rows, the wall's own list in grid. */
  /* QUICK LOOK PAGES THROUGH THE SELECTION (user 2026-09-23: *"multiple files selected should
   * show '1/2 <>' … because two files are highlighted"*). With more than one file selected the set
   * is the selection, in list order, starting on the file asked for; otherwise it is the list the
   * caller handed over. Every open goes through here so no view pages a different set. */
  const openQuickLook = (q) => {
    if (!q) { setQuickLook(null); return }
    /* in the order on screen when the caller's list holds the whole selection, else bucket order */
    const inView = q.files.filter((o) => rowSelection.has(o.key))
    const sel = inView.length === rowSelection.size ? inView : objects.filter((o) => rowSelection.has(o.key))
    const current = q.files[q.index]
    if (current?.key) logEvent('opened', current.key)
    if (sel.length > 1) {
      const index = Math.max(0, sel.findIndex((o) => o.key === current?.key))
      setQuickLook({ files: sel, index })
      return
    }
    setQuickLook(q)
  }
  /* QUICK LOOK STEPS THE SELECTION TOO (user 2026-09-23: *"when quicklook preview is open on
   * selected item and arrow right focuses new item, that item is being selected"*). Paging a folder,
   * the file you land on becomes the selection and the pick — so closing the window leaves you on
   * it. Paging a multi-selection, the set is what you are looking through and stays whole. */
  /* closing hands focus to the SELECTION, not back to the tile that opened the window — after
   * stepping they are different tiles, and the overlay's own focus-return would light the old one */
  const closeQuickLook = () => {
    setQuickLook(null)
    requestAnimationFrame(() => {
      const key = selectAnchorRef.current
      const el = key && document.querySelector(`[data-marquee-key="${CSS.escape(key)}"]`)
      if (el?.tabIndex >= 0) el.focus({ preventScroll: true })
    })
  }
  const stepQuickLook = (dir) => {
    if (!quickLook) return
    const { files } = quickLook
    const index = (quickLook.index + dir + files.length) % files.length
    setQuickLook({ ...quickLook, index })
    const o = files[index]
    if (o && rowSelection.size <= 1) {
      setRowSelection(new Set([o.key])); selectAnchorRef.current = o.key
      setPickedFolder(null); setPickedFile(o)
    }
  }
  const spaceQuickLook = () => {
    const key = pickedFile?.key ?? pickedFolder ?? (rowSelection.size === 1 ? [...rowSelection][0] : null)
    /* a picked bucket or the title Quick-Looks like any folder (user 2026-09-25) */
    if (!key && previewRoot) { setQuickLookFolder(ROOT_KEY); return }
    if (!key) return
    /* A FOLDER QUICK-LOOKS TOO (user 2026-09-23: *"anything can be quick viewed, also folders"*) —
     * the same pane the preview column draws, at overlay size. */
    if (key.endsWith('/')) { setQuickLookFolder(key); return }
    const files = isWall
      ? wallFiles
      : rowLevel(key.slice(0, key.lastIndexOf('/') + 1), sortedObjects).files
    const index = files.findIndex((f) => f.key === key)
    if (index === -1) return
    openQuickLook({ files, index })
  }

  /* SEARCH IS THE DS'S OWN MODAL (2026-09-22). `/` is the file-manager habit and ⌘K the app one,
   * so both open it; it searches the bucket's keys and JUMPS — it does not filter the list under
   * you, which is what the filter bar is for. */
  const [searchOpen, setSearchOpen] = useState(false)
  const [searchQuery, setSearchQuery] = useState('')
  /* the filter bar's own query — lifted so a typed one swaps the tree for a flat list, and so the
   * search modal's ⌘Enter can hand its query over */
  const [barQuery, setBarQuery] = useState('')
  /* THE ARROWS IN THE GRID AND THE ROWS (user 2026-09-23: *"arrow navigation doesnt work in grid
   * mode? nor in row mode"*). The columns have always had theirs (`ColumnBrowser`); these are the
   * same keys for the other two views, and every one is listed in the shortcuts sheet.
   *   grid   ←→ one tile, ↑↓ one row of tiles (the grid's live column count)
   *   rows   ↑↓ the visible order · → expands a folder, again steps into it · ← collapses, or goes
   *          to the parent folder
   *   both   ⌘↑ the enclosing folder · ⌘↓ opens (a folder in the rows, Quick Look on a file) */
  const gridOrderRef = useRef([])
  const arrowNav = (e) => {
    if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) return false
    /* ⇧+ARROWS KEEP SELECTING UNDER QUICK LOOK (user 2026-09-24), Finder's: the selection grows
     * underneath and the window pages through it */
    if (view === 'columns' || (quickLook && !e.shiftKey) || quickLookFolder || searchOpen) return false
    if (atTitleRoot && !single) {
      if (e.metaKey && e.key === 'ArrowUp') goUp()
      else if (e.metaKey && e.key === 'ArrowDown') { if (atTop) goTitleRoot(); else openBucket(bucketList.find((b) => (b.id ?? b.label) === pickedBucket) ?? bucketMeta) }
      return true
    }
    const cur = selectCursorRef.current ?? selectAnchorRef.current ?? [...rowSelection].pop() ?? null
    /* ⇧ + ARROW EXTENDS (user 2026-09-23: *"shift up down or left right … should also make
     * selection"*): the anchor stays where the run began, the cursor moves, and the selection is
     * everything between them in the order on screen. */
    const extend = (order, key) => {
      const anchor = selectAnchorRef.current ?? cur ?? key
      const a = order.indexOf(anchor)
      const b = order.indexOf(key)
      if (a === -1 || b === -1) return false
      setRowSelection(new Set(order.slice(Math.min(a, b), Math.max(a, b) + 1)))
      selectAnchorRef.current = anchor
      selectCursorRef.current = key
      return true
    }
    /* FOCUS FOLLOWS THE SELECTION (user 2026-09-23: *"why are we supporting multiple selected/highlight
     * states?"*). The clicked tile kept keyboard focus — and its focus ring — while the arrows moved
     * the selection on, so two tiles looked current. The one the arrows land on takes focus too. */
    const reveal = (box, key) => {
      const el = box?.querySelector(`[data-marquee-key="${CSS.escape(key)}"]`)
      el?.scrollIntoView({ block: 'nearest' })
      if (el?.tabIndex >= 0) el.focus({ preventScroll: true })
    }
    if (view === 'grid') {
      /* BEFORE the empty-list bail-out: a folder that holds only subfolders has an empty wall, and
       * ⌘↑ was swallowed there */
      if (e.metaKey && e.key === 'ArrowUp') { goUp(); return true }
      /* THE ARROWS WALK WHAT IS DRAWN (user 2026-09-24: they skipped every folder tile) — the
       * folders, then the files, in the order on screen. The list was the files alone, from before
       * the grid drew folders. */
      const files = gridOrderRef.current
      const items = [...(settings.flat ? [] : folders.map((f) => ({ key: prefix + f, folder: true }))), ...files.map((o) => ({ key: o.key, o }))]
      if (!items.length) return true
      const i = items.findIndex((it) => it.key === cur)
      if (e.metaKey && e.key === 'ArrowDown') {
        const it = items[i]
        if (it?.folder) goFolder(it.key)
        else if (it) openQuickLook({ files, index: Math.max(0, files.findIndex((f) => f.key === it.key)) })
        return true
      }
      const box = gridMarquee.ref.current
      /* the band lives on the pane now; the column count is the tile grid's, inside it */
      const tiles = box?.querySelector('.grid')
      const cols = tiles ? getComputedStyle(tiles).gridTemplateColumns.split(' ').filter(Boolean).length : 1
      const step = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -cols, ArrowDown: cols }[e.key]
      const it = items[i === -1 ? 0 : Math.max(0, Math.min(items.length - 1, i + step))]
      if (e.shiftKey && extend(items.map((x) => x.key), it.key)) { reveal(box, it.key); return true }
      setRowSelection(new Set([it.key])); selectAnchorRef.current = it.key; selectCursorRef.current = null
      if (it.folder) { setPickedFile(null); setPickedFolder(it.key) } else { setPickedFolder(null); setPickedFile(it.o) }
      reveal(box, it.key)
      return true
    }
    if (view === 'rows') {
      const order = visibleRowOrder(sortedObjects)
      if (!order.length) return true
      const select = (key) => {
        setRowSelection(new Set([key])); selectAnchorRef.current = key; selectCursorRef.current = null
        if (key.endsWith('/')) rowPickFolder(key)
        else { const o = objects.find((x) => x.key === key); if (o) rowPickFile(o) }
        reveal(rowsMarquee.ref.current, key)
      }
      const isFolder = cur?.endsWith('/')
      const open = isFolder && rowsExpanded.has(cur)
      if (e.metaKey && e.key === 'ArrowUp') { const up = cur ? dirOf(cur) : dirOf(prefix); if (up) select(up); else goUp(); return true }
      if (e.metaKey && e.key === 'ArrowDown') {
        if (!cur) return true
        if (isFolder) goFolder(cur)
        else { const lvl = rowLevel(dirOf(cur), sortedObjects).files; openQuickLook({ files: lvl, index: Math.max(0, lvl.findIndex((f) => f.key === cur)) }) }
        return true
      }
      const i = order.indexOf(cur)
      if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
        const next = order[i === -1 ? 0 : Math.max(0, Math.min(order.length - 1, i + (e.key === 'ArrowDown' ? 1 : -1)))]
        if (e.shiftKey && extend(order, next)) { reveal(rowsMarquee.ref.current, next); return true }
        select(next); return true
      }
      if (e.key === 'ArrowRight' && isFolder) {
        if (!open) setExpandedRows((prev) => new Set([...prev, cur]))
        else if (order[i + 1]?.startsWith(cur)) select(order[i + 1])
        return true
      }
      if (e.key === 'ArrowLeft' && cur) {
        if (open) toggleRow(cur)
        else if (dirOf(cur)) select(dirOf(cur))
        return true
      }
      return true
    }
    return false
  }
  useEffect(() => {
    const onKey = (e) => {
      const el = e.target
      if (el?.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(el?.tagName ?? '')) return
      if (e.key === '/' && !e.metaKey && !e.ctrlKey) { e.preventDefault(); setSearchOpen(true) }
      if (e.key === 'k' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); setSearchOpen(true) }
      if (arrowNav(e)) { e.preventDefault(); return }
      /* ENTER OPENS THE SELECTED FOLDER, ⇧ENTER GOES TO THE PARENT (user 2026-09-24). The same
       * two moves as a double-click and ⌘↑, so the child you leave is still the one selected.
       * A focused tile has already opened itself on Enter (it calls preventDefault). */
      if (e.key === 'Enter' && !e.metaKey && !e.ctrlKey && !e.altKey && !e.defaultPrevented && !quickLook && !quickLookFolder && !searchOpen) {
        if (e.shiftKey) { e.preventDefault(); goUp(); return }
        if (view !== 'columns') {
          const root = rootItems.find((it) => it.key === pickedBucket)
          const sel = rowSelection.size === 1 ? [...rowSelection][0] : pickedFolder
          if (root) { e.preventDefault(); root.open(); return }
          if (sel?.endsWith('/')) { e.preventDefault(); goFolder(sel); return }
        }
      }
      /* the columns keep their own space handler — theirs knows the column's cursor */
      if (e.key === ' ' && view !== 'columns' && !searchOpen) {
        e.preventDefault()
        if (quickLook) closeQuickLook()
        else if (quickLookFolder) setQuickLookFolder(null)
        else spaceQuickLook()
      }
    }
    window.addEventListener('keydown', onKey)
    return () => window.removeEventListener('keydown', onKey)
  })

  const [quickLook, setQuickLook] = useState(null)
  const [quickLookFolder, setQuickLookFolder] = useState(null)
  /* …and the open window takes the new selection as its set, on the file the cursor reached */
  useEffect(() => {
    if (!quickLook || rowSelection.size < 2) return
    const order = view === 'grid' ? gridOrderRef.current : sortedObjects
    const files = order.filter((o) => rowSelection.has(o.key) && !o.key.endsWith('/'))
    if (files.length < 2) return
    if (files.length === quickLook.files.length && files.every((f, i) => f.key === quickLook.files[i].key)) return
    const at = files.findIndex((f) => f.key === selectCursorRef.current)
    setQuickLook({ files, index: at === -1 ? 0 : at })
  }, [rowSelection]) // eslint-disable-line react-hooks/exhaustive-deps
  const columnsRef = useRef(null)
  /* the view box (whichever view is mounted) and the page root — what `fill` measures */
  const [viewEl, viewRef] = useState(null)
  /* stable, so the columns box is not re-attached (null, then itself) on every render */
  const columnsBox = useCallback((el) => { columnsRef.current = el; viewRef(el) }, [])
  const pageRef = useRef(null)
  const fill = settings.columnHeight === 'fill'
  const fillH = useFillHeight(fill, viewEl, pageRef)
  /* a drag in fill mode is the SESSION's, never saved — one height on every reload (apps/media's ruling) */
  const [dragH, setDragH] = useState(null)
  const colH = fill ? (dragH ?? fillH ?? SETTINGS_BASE.columnHeight) : (settings.columnHeight ?? SETTINGS_BASE.columnHeight)
  const onColH = (px) => (fill ? setDragH(px) : setSettings({ ...settings, columnHeight: px }))
  /* `flat` IS THE WALL'S, NOT THIS PAGE'S (user 2026-09-22: "there is no option to switch to flat
   * mode, bc its not in files view. this is row mode.. this makes no sense"). It reached here from
   * kol-r2b2's single surface and did two things that cannot be defended in a TREE: it struck every
   * folder name through, and it counted the whole subtree in the level's own count line — with the
   * only control on the other view's filter bar. A tree browses folders; flat browses files. */
  /* `view` IS THE STATE; `folderView` is what it used to be called for the two tree views and is
   * kept in step so an older consumer (kol-r2b2, kol-olina) is unaffected. */
  const stored = viewProp ?? settings.view ?? (settings.folderView === 'rows' ? 'rows' : 'columns')
  const view = stored === 'list' ? 'rows' : stored
  const isWall = view === 'grid'
  const folderView = view === 'rows' ? 'rows' : 'columns'
  /* BELOW `md` BOTH TREE VIEWS ARE THE PHONE LIST. `ColumnBrowser` already forks to its one-level
   * stack there; the row view had no phone form and drew the desktop list and its preview pane
   * squeezed into 390 — 35px of rows with no names. Same query, same breakpoint as the columns. */
  const phone = useMediaQuery('(max-width: 767px)')
  /* THE COUNT LINE'S AUTO (apps review 2026-09-29): a desk reads it, a phone spends a line on it */
  const coarse = useMediaQuery('(pointer: coarse)')
  const countLine = settings.countLine ?? 'auto'
  const showCount = countLine === 'on' || (countLine === 'auto' && !phone && !coarse)
  const setView = (v) => {
    onViewChange?.(v)
    setSettings({
      ...settings,
      view: v,
      folderView: v === 'rows' ? 'rows' : v === 'columns' ? 'columns' : settings.folderView,
      layout: v === 'grid' ? 'grid' : settings.layout,
    })
  }
  /* ROW VIEW'S PREVIEW PANE. Reserved whenever the setting is on, empty until something is picked
   * — Finder keeps the space rather than reflowing the list on every selection. */
  const showRowPreview = folderView === 'rows' && (settings.rowPreview ?? true)
  const showGridPreview = view === 'grid' && (settings.rowPreview ?? true)
  /* OFF by default: a row is a name, and the facts live in the preview (user 2026-09-22) */
  const rowCols = settings.rowColumns ? { ...rowColWidths(settings), ...(hasTags && { tags: settings.rowColumnWidths?.tags ?? 140 }) } : null
  const previewWidth = settings.columnWidths?.preview ?? 320
  const previewFile = pickedFile && pickedFile.key.startsWith(prefix) ? pickedFile : null
  const previewFolder = pickedFolder && pickedFolder.startsWith(prefix) ? pickedFolder : null
  /* a bucket's totals come from `folderTree` when the consumer passes one; the open bucket can
   * always count its own listing */
  const bucketTotals = (id) => {
    const t = folderTree?.[id]
    if (t && t.files != null) return { files: t.files, bytes: t.bytes ?? 0 }
    if (id === bucketMeta.id) return { files: objects.length, bytes: objects.reduce((n, o) => n + (o.size || 0), 0) }
    return null
  }
  /* THE PREVIEW PANE, ONE FUNCTION FOR THE ROWS AND THE GRID (user 2026-09-23: *"why did you not
   * treat this like column and row, with container padding + preview pane? CONSISTENCY"*). Several
   * selected → the stack; a file → its preview; a folder → its summary; nothing → the reserved
   * space, which Finder keeps rather than reflowing the list on every pick. */
  const previewPane = () => (
    rowSelection.size > 1 ? (
      <SelectionPreview keys={[...rowSelection]} objects={objects} urlOf={(o) => mediaUrl(o.key)} kindOf={kindOf} formatSize={formatSize} formatDate={formatDate} width={previewWidth} />
    ) : previewFile ? (
      <ColumnPreview key={previewFile.key} o={previewFile} urlOf={(o) => mediaUrl(o.key)} kindOf={kindOf} kindLabel={KIND_LABEL}
        formatSize={formatSize} formatDate={formatDate} width={previewWidth}
        renderDetails={detailsFor}
        renderPreview={(o) => {
          if (isImage(o.contentType)) return <ImageFrame src={mediaUrl(o.key)} vector={o.contentType === 'image/svg+xml'} />
          const poster = posterFor(o.key, keySet)
          return <KindPreview o={o} urlOf={(x) => mediaUrl(x.key)} poster={poster ? mediaUrl(poster) : undefined} kindOf={kindOf} kindLabel={KIND_LABEL} />
        }} />
    ) : previewFolder ? (
      <FolderPreview key={previewFolder} path={previewFolder} objects={objects} width={previewWidth} formatDate={formatDate} details={detailsFor(previewFolder)} />
    ) : previewRoot ? (
      <ContainerPreview key={previewRoot.key} name={previewRoot.name} icon={previewRoot.icon} facts={previewRoot.facts} width={previewWidth} />
    ) : (
      <div className="kol-column-browser-preview shrink-0" style={{ width: previewWidth }} />
    )
  )
  /* PINNED SEARCH + `···` (item 15), below `md` only. Item 7 of the previous
   * ticket hid the desktop control cluster and put nothing in its place, so
   * SORT became unreachable on a phone; both references carry exactly these two
   * controls above the list. Search is page state, not a setting — a query is
   * something you are doing, not something you have configured. */
  const [query, setQuery] = useState('')

  /* Search filters the KEY SPACE, so the tree still navigates: a path survives
   * when a file under it matches and ColumnBrowser's own partition does the
   * rest. One view, rather than a flat results list nobody asked for. Sort
   * orders the files the way the wall already sorts its own. */
  const q = query.trim()
  /* SEGMENTS HIDE WHEN FOLDED, in every view (user 2026-09-23: *"we can just by default hide the
   * segments, if they serve no practical purpose"*). A `.ts` chunk cannot play on its own — its
   * stream's `.m3u8` beside it is the thing you open. The fold used to reach only the grid, so the
   * columns and rows listed every chunk. */
  const listed = useMemo(() => (settings.foldSegments ? objects.filter((o) => !isSegment(o.key)) : objects), [objects, settings.foldSegments])
  const searched = useMemo(() => filterMedia(listed, q), [listed, q])
  const sortedObjects = useMemo(
    () => sortObjects(searched, settings.sortBy, settings.sortDir),
    [searched, settings.sortBy, settings.sortDir],
  )

  const treeFolders = (level) => (folderTree?.[bucketMeta.id]?.folders ?? [])
    .filter((p) => p.startsWith(level) && p.length > level.length && !p.slice(level.length, -1).includes('/'))
    .map((p) => p.slice(level.length))

  const scoped = prefix ? sortedObjects.filter((o) => o.key.startsWith(prefix)) : sortedObjects
  const { folders: keyFolders, files: dirFiles } = partition(scoped, prefix)
  /* THE COUNT COUNTS WHAT IS ON SCREEN. `partition` derives folders from file keys, so an EMPTY
   * folder is invisible to it — the stats line said "2 folders" under a list showing three, the
   * third being `03-scratch/`, which exists in `folderTree` and nowhere else. Both views draw the
   * merged set now, so the tally is taken from the same place. */
  const folders = [...new Set([...treeFolders(prefix), ...keyFolders])].sort()
  const keySet = new Set(objects.map((o) => o.key))
  const levelFiles = dirFiles
  const systemCount = levelFiles.filter((o) => isSystemFile(o.key)).length
  const rawFiles = levelFiles.filter((o) => !isSystemFile(o.key)).map((o) => ({ ...o, kind: kindOf(o) }))
  const profile = profileOf(objects, rawFiles, systemCount)
  const totalBytes = rawFiles.reduce((n, o) => n + (o.size ?? 0), 0)
  const bucketFiles = objects.length
  const bucketBytes = objects.reduce((n, o) => n + (o.size || 0), 0)
  const crumbs = prefix ? prefix.replace(/\/$/, '').split('/') : []

  /* the virtual root — one browser for the whole client: root row = the title,
   * its children the buckets, stepping into one IS the bucket switch.
   *
   * ONE BUCKET GETS NO BUCKET LEVEL (one-bucket-consumer, kol-client-olina
   * 2026-09-03): with a single bucket the second column was a row that named
   * the only thing it could name, under a root that named the app — two levels
   * to reach a folder. The virtual root collapses to the empty string, which
   * makes every `slice(VROOT.length)` below a no-op and leaves column 0 as the
   * bucket's own folders. Multi-bucket browse is untouched. */
  const single = !bucketLevel && buckets.length <= 1
  const ROOT = title
  const label = bucketMeta.label || 'bucket'
  const VROOT = single ? '' : `${ROOT}/${label}/`
  /* THE PICKER'S TILES AND COUNTS BY DEFAULT (user 2026-10-23, the Hub's Media on a phone beside kol-website's media app:
   * folders read bare, files drew a kind glyph). A consumer's `thumbnailFor` / `folderMeta` still win; without them an
   * image file shows itself (the client's URL, lazy) and a folder counts the files under it in the loaded listing. */
  const thumbOf = thumbnailFor ?? ((o) => (hasUrl && kindOf(o) === 'image'
    ? <img src={mediaUrl(unroot(o.key))} alt="" loading="lazy" decoding="async" className="w-full h-full object-cover" />
    : null))
  const metaOf = folderMeta ?? ((path) => {
    const pre = unroot(path).replace(/\/?$/, '/')
    const n = objects.filter((o) => o.key.startsWith(pre) && !o.key.endsWith('/')).length
    return n ? `${n} item${n === 1 ? '' : 's'}` : ''
  })
  const unroot = (p) => (VROOT && p.startsWith(VROOT) ? p.slice(VROOT.length) : p)
  /* ONE LEVEL, ONE ANSWER. The column view merges the baked tree with the live partition inside its
   * `partition` prop; the row view used the live partition alone and therefore disagreed with the
   * columns about what a folder even is. This is that merge, lifted so both callers share it —
   * folders from `folderTree` (real nodes, empty ones included) unioned with the ones derived from
   * keys, plus the level's own files, which the row view never rendered at all. */
  const rowLevel = (level, list = objects) => {
    /* `partition` slices every key by `prefix` WITHOUT checking it matches, so it must be handed an
     * already-scoped list. Given the whole bucket it cut four characters off every root key and
     * drew `readme.md` as `me.md` and `video/` as `o/`. Scope first, always. */
    const inLevel = level ? list.filter((o) => o.key.startsWith(level)) : list
    const live = partition(inLevel, level)
    const baked = treeFolders(level)
    return {
      folders: [...new Set([...baked, ...live.folders])].sort(),
      files: sortObjects(live.files.filter((o) => !isSystemFile(o.key)), settings.sortBy, settings.sortDir),
    }
  }

  useEffect(() => {
    if (folderView !== 'columns') return undefined
    const root = columnsRef.current?.querySelector('[tabindex="0"]')
    if (!root) return undefined
    const id = requestAnimationFrame(() => root.scrollTo({ left: root.scrollWidth, behavior: 'smooth' }))
    return () => cancelAnimationFrame(id)
  }, [folderView, prefix, atTitleRoot, pickedFile, bucketMeta.id])

  if (error) return <p className="kol-mono-12 text-ui-error">Error: {error}</p>
  /* NOT SELECTABLE (user 2026-09-23, superseding 2026-09-22's selectable crumbs: *"instead of this
   * highlight, could we make a hover click to copy path?"*). A drag across the line painted the
   * browser's selection block over it; the path is one click on the current crumb now. */
  const crumbCls = (active) => `cursor-pointer select-none transition-colors ${active ? 'text-oq-96' : 'text-oq-48 hover:text-oq-64'}`
  const fullPath = (tail = '') => [ROOT, ...(atTitleRoot ? [] : [label, ...crumbs]), ...(tail ? [tail] : [])].join('/')

  /* ── THE WALL'S LIST, on the same surface (2026-09-22) ──────────────────────────────────────
   * `flat` belongs to the FILE views and not to the tree (that ruling stands): a wall shows the
   * folder's own files, or the whole subtree when flat is on. Grouping and segment-folding are the
   * settings the wall always had. */
  const wallKinds = new Set(settings.kinds ?? SETTINGS_BASE.kinds)
  const wallSource = settings.flat
    ? scoped.map((o) => ({ ...o, displayKey: o.displayName ?? (prefix ? o.key.slice(prefix.length) : o.key) }))
    : dirFiles
  const wallVisible = wallKinds.has('system') ? wallSource : wallSource.filter((o) => !isSystemFile(o.key))
  const wallGrouped = settings.groupVariants ? groupVariants(wallVisible) : wallVisible
  const wallFiles = (settings.foldSegments ? groupSegments(wallGrouped) : wallGrouped)
    .map((o) => ({ ...o, kind: kindOf(o), poster: posterFor(o.key, keySet) }))
  const filtersOn = !!settings.filters
  /* the bar filters FILES, and a tree is drawn from the files under it — so one filtered list
   * serves both: the wall renders it, the tree is scoped to the keys in it.
   * THE TREE'S LIST IS THE WHOLE BUCKET, not the folder you stand in (user 2026-09-30: open the
   * bar at `brand/og/` and `logos`, `original`, `projects` vanished with no filter set). The
   * columns draw every level up to the root, so a list scoped to `prefix` cut every sibling path
   * the moment the bar mounted. */
  const filterItems = isWall ? wallFiles : sortedObjects.filter((o) => !isSystemFile(o.key)).map((o) => ({ ...o, kind: kindOf(o), poster: posterFor(o.key, keySet), displayKey: o.displayName ?? o.key }))
  const filterKinds = [...new Set(filterItems.map((o) => o.kind))].sort()
  const filterTags = [...new Set(filterItems.flatMap((o) => o.tags ?? []))].sort()
  /* favourites as a one-chip group: ContentFilters matches a value, so a starred file carries one */
  const filterItemsFav = filterItems.map((o) => ({ ...o, starred: o.favourite ? 'favourite' : '' }))
  const anyStarred = filterItemsFav.some((o) => o.starred)

  /* ── THE BODY, one of four (2026-09-22) ────────────────────────────────────────────────────
   * `treeBody` draws the columns or the rows over whatever object list it is handed — the whole
   * key space, or the subset the filter bar left standing. `wallPane` is the same two file views
   * the Files page rendered, in a pane of the browser's own height so the page does not jump
   * between views. */
  /* AT THE TITLE ROOT, ROWS AND GRID LIST THE BUCKETS (same ticket: they listed the bucket's
   * folders under a crumb that named the title). One row / tile per bucket; a click opens it. */
  const bucketList = buckets.length ? buckets : [bucketMeta]
  const titleLevel = atTitleRoot && !single
  /* what the root levels hold: above the title, the title as one folder; inside it, the buckets.
   * Each is a folder with its own icon — a click highlights it, a double-click opens it. */
  const previewRoot = (() => {
    if (!titleLevel || pickedBucket == null) return null
    if (atTop && pickedBucket === ROOT_KEY) {
      const all = bucketList.map((b) => bucketTotals(b.id)).filter(Boolean)
      return { key: ROOT_KEY, name: ROOT, icon: 'folder', facts: [
        ['Kind', 'folder'],
        ['Buckets', `${bucketList.length}`],
        ...(all.length === bucketList.length ? [['Items', `${all.reduce((n, x) => n + x.files, 0)}`], ['Size', formatSize(all.reduce((n, x) => n + x.bytes, 0))]] : []),
      ] }
    }
    const b = bucketList.find((x) => (x.id ?? x.label) === pickedBucket)
    if (!b) return null
    const tot = bucketTotals(b.id)
    return { key: pickedBucket, name: b.label || 'bucket', icon: 'database', facts: [
      ['Kind', 'bucket'],
      ...(tot ? [['Items', `${tot.files}`], ['Size', formatSize(tot.bytes)]] : []),
      ...(b.writable != null ? [['Access', b.writable ? 'read · write' : 'read-only']] : []),
    ] }
  })()
  const rootItems = !titleLevel ? [] : atTop
    ? [{ key: ROOT_KEY, name: ROOT, icon: 'folder', open: goTitleRoot }]
    : bucketList.map((b) => ({ key: b.id ?? b.label, name: b.label || 'bucket', icon: 'database', open: () => openBucket(b) }))
  const treeBody = (keep) => {
    const treeObjects = keep ? sortedObjects.filter((o) => keep.has(o.key)) : sortedObjects
    return (
  folderView === 'columns' || phone ? (
            <div ref={columnsBox} className="relative">
              <ColumnBrowser
                autoFocus={autoFocus}
                className={atTitleRoot ? 'is-root' : ''}
                tapSelects={!!onPickFile}
                height={colH}
                onHeightChange={onColH}
                rowSize={rowSize}
                columnWidths={settings.columnWidths}
                onColumnResize={(i, px) => setSettings({ ...settings, columnWidths: { ...(settings.columnWidths ?? {}), [i]: px } })}
                objects={treeObjects.filter((o) => !isSystemFile(o.key)).map((o) => ({ ...o, key: `${VROOT}${o.key}` }))}
                prefix={single ? prefix : (atTitleRoot ? `${ROOT}/` : `${VROOT}${prefix}`)}
                onPrefix={(v) => {
                  /* single: the browser's path IS the bucket path, no segment to strip */
                  if (single) { setAppRoot(false); setPrefix(v); return }
                  const [, seg, ...rest] = v.split('/')
                  const target = buckets.find((b) => b.label === seg)
                  if (!target && buckets.length) { setAppRoot(true); setPickedFile(null); setPrefix(''); return }
                  setAppRoot(false)
                  const real = rest.join('/')
                  if (target && target.id !== bucketMeta.id) switchBucket(target.id, real); else setPrefix(real)
                }}
                picked={pickedFile ? { ...pickedFile, key: `${VROOT}${pickedFile.key}` } : null}
                onPick={(o) => setPickedFile(o ? { ...o, key: o.key.slice(VROOT.length) } : null)}
                quickLook={quickLook && { ...quickLook, files: quickLook.files.map((o) => ({ ...o, key: `${VROOT}${o.key}` })) }}
                onQuickLook={(q) => openQuickLook(q && { ...q, files: q.files.map((o) => ({ ...o, key: o.key.slice(VROOT.length) })) })}
                urlOf={(o) => mediaUrl(o.key.slice(VROOT.length))}
                kindOf={kindOf}
                kindLabel={KIND_LABEL}
                formatSize={formatSize}
                formatDate={formatDate}
                thumbnailFor={thumbOf}
                folderMeta={metaOf}
                /* the PROP wins when a consumer passes one; otherwise the `···`
                   owns it through settings (item 15) */
                stackView={stackView ?? settings.stackView ?? 'list'}
                renderDetails={(o) => detailsFor({ ...o, key: o.key.slice(VROOT.length) })}
                renderPreview={(o) => {
                  const real = { ...o, key: o.key.slice(VROOT.length) }
                  if (isImage(real.contentType)) return <ImageFrame src={mediaUrl(real.key)} vector={real.contentType === 'image/svg+xml'} />
                  const poster = posterFor(real.key, keySet)
                  return <KindPreview o={real} urlOf={(x) => mediaUrl(x.key)} poster={poster ? mediaUrl(poster) : undefined} kindOf={kindOf} kindLabel={KIND_LABEL} />
                }}
                /* THE COLUMNS SPEAK IN VIRTUAL PATHS. With more than one bucket every level is
                  * prefixed `<title>/<bucket>/` so the stores share one tree — so a path coming OUT
                  * of the browser must be un-rooted before it reaches a file verb, or the store is
                  * asked to move `MEDIA/R2 · kol-media/img/x.jpg`, which it has never heard of. The
                  * row view needs none of this; it is already bucket-relative. */
                onRowContextMenu={(e, payload) => menu.openAt(e, { ...payload, path: unroot(payload.path), ...(payload.o && { o: { ...payload.o, key: unroot(payload.o.key) } }) })}
                dragFor={dragFor ? (path) => dragFor(unroot(path)) : undefined}
                /* virtual paths again: only a level inside THIS bucket's root can take a file */
                onDropFiles={dropFilesTo ? (files, path) => { if (!VROOT || path.startsWith(VROOT)) dropFilesTo(unroot(path))(files) } : undefined}
                /* the band's keys come back virtual-rooted; the page keeps ONE selection, bucket-relative */
                selectedKeys={VROOT ? new Set([...rowSelection].map((k) => `${VROOT}${k}`)) : rowSelection}
                onSelectKeys={(keys, additive) => { marqueeSelect(keys.map(unroot), additive); if (!additive && keys.length === 1) selectAnchorRef.current = unroot(keys[0]) }}
                onSelectClick={(key, e, keys) => selectRow(unroot(key), e, keys.map(unroot))}
                /* the bucket rows under the title are SOURCES — `database`, not `folder` */
                folderIcon={(path) => (!single && path.startsWith(`${ROOT}/`) && path.slice(ROOT.length + 1).split('/').filter(Boolean).length === 1 ? 'database' : 'folder')}
                partition={(objs, level) => {
                  if (!single) {
                    if (level === '') return { folders: [`${ROOT}/`], files: [] }
                    if (level === `${ROOT}/`) return { folders: (buckets.length ? buckets : [bucketMeta]).map((b) => `${b.label || 'bucket'}/`), files: [] }
                  }
                  const live = partition(objs, level)
                  const baked = treeFolders(level.slice(VROOT.length))
                  return { folders: [...new Set([...baked, ...live.folders])].sort(), files: live.files }
                }}
              />
            </div>
          ) : (
            /* ROW VIEW — the same level the column view draws, not a different one. It used to render
             * `partition(scoped, prefix).folders` and nothing else, which lost two things: an EMPTY
             * folder (partition derives folders from file keys, so a folder with no files under it
             * does not exist to it — `folderTree` is where real nodes live, and the column view was
             * already merging the two) and every FILE at the level. `#img/` drew 2 rows against the
             * columns' 4. Folders first, then files, each expandable in place like a list view. */
            /* SAME AREA AS THE COLUMNS. The row view used to be content-height, so the two folder
             * views made the page jump between them and the blank space below the last row — where
             * you right-click for "new folder" and drop to file something here — did not exist. It
             * takes the column browser's own height now, from the same setting.
             *
             * The list IS the current folder, so it is a drop target for `prefix`, matching a column
             * being a target for its level. Highlight via a data attribute for the same reason.
             *
             * THE COLUMN BROWSER'S PANE (user 2026-09-22): same border, radius and height, the list a
             * `kol-column-browser-column` (its 4px block inset, its selected rules), and beside it the
             * same preview the columns open — Finder's "Show preview", on by default (`rowPreview`). */
            /* THE WRAPPER OWNS THE HEIGHT, not the pane (2026-09-22). Both folder views have to end on
             * the same line — the count line under them jumps otherwise, and a consumer's fill height
             * is ONE value for both — so the header comes out of this view's budget rather than being
             * added to it. The pane takes what is left. */
            <div ref={viewRef} className="flex flex-col gap-2" style={{ height: colH }}>
            {/* the header labels the LIST, so it spans the list and stops where the preview starts */}
            {rowCols && (
              <RowHeader sortBy={settings.sortBy} sortDir={settings.sortDir} width={showRowPreview ? `calc(100% - ${previewWidth}px)` : '100%'}
                cols={rowCols}
                onResize={(key, px) => setSettings({ ...settings, rowColumnWidths: { ...rowColWidths(settings), [key]: px } })}
                onSort={(sortBy, sortDir) => setSettings({ ...settings, sortBy, sortDir })} />
            )}
            <div className="relative border rounded flex overflow-hidden flex-1 min-h-0"
              style={{ borderColor: 'var(--kol-oq-08)' }}>
            <div className={`flex-1 min-w-0 flex flex-col${showRowPreview ? ' border-r' : ''}`} style={{ borderColor: 'var(--kol-oq-08)' }}>
            <ul ref={rowsMarquee.ref} {...rowsMarquee.props}
            /* the background deselects, here as in the columns */
            onClick={(e) => { if (!e.target.closest('[data-marquee-key]')) { setRowSelection(new Set()); setPickedFile(null); setPickedFolder(null); setPickedBucket(null) } }}
            className="relative kol-column-browser-column kol-row-browser flex-1 min-w-0 flex flex-col overflow-y-auto"
              onContextMenu={titleLevel ? undefined : (e) => menu.openAt(e, { type: 'level', path: prefix })}
              onDragOver={(e) => {
                const files = dropFilesTo && isFileDrag(e)
                const d = !isFileDrag(e) && dragFor?.(prefix)
                if (!files && !d?.canDrop(prefix)) return
                e.preventDefault()
                e.currentTarget.dataset.dropOver = '1'
              }}
              /* THE HIGHLIGHT CLEARS HOWEVER THE DRAG ENDS (user 2026-09-22 — it stuck as a border).
               * A drop on a folder row stops propagation, so this list's own onDrop never ran; the
               * capture phase runs first. Leaving through a child fires on the child, so leave is
               * "the pointer is no longer inside", not "the event target was the list". */
              onDragLeave={(e) => { if (!e.currentTarget.contains(e.relatedTarget)) delete e.currentTarget.dataset.dropOver }}
              onDropCapture={(e) => { delete e.currentTarget.dataset.dropOver }}
              onDrop={(e) => {
                if (dropFilesTo && isFileDrag(e)) { e.preventDefault(); dropFilesTo(prefix)(e.dataTransfer.files); return }
                const d = !isFileDrag(e) && dragFor?.(prefix)
                if (!d?.canDrop(prefix)) return
                e.preventDefault()
                d.onDrop(prefix)
              }}>
              {rowsMarquee.rect && <div className="kol-marquee" style={rowsMarquee.rect} />}
            {titleLevel ? rootItems.map((it) => (
                <FolderRow key={it.key} name={it.name} icon={it.icon} cols={rowCols}
                  selected={pickedBucket === it.key}
                  onClick={(e) => { e.stopPropagation(); setPickedBucket(it.key) }} onDoubleClick={it.open} />
              )) : <>
            {rowLevel('', treeObjects).folders.map((f) => {
                const path = f
                const open = rowsExpanded.has(path)
                const order = visibleRowOrder(treeObjects)
                return (
                  <Fragment key={f}>
                    <FolderRow
                      name={f} expanded={open}
                      markKey={path}
                      /* THE FOLDER YOU ARE IN IS NOT A SELECTION (user 2026-09-23) — it wears the trail
                       * mark, the columns' parent-folder tone, never the selected fill */
                      selected={rowSelection.has(path) || pickedFolder === path}
                      current={prefix === path}
                      onContextMenu={(e) => menu.openAt(e, { type: 'folder', path, targets: targetsFor(path) })}
                      drag={dragFor?.(path)}
                      dropFiles={dropFilesTo?.(path)}
                      meta={metaOf(path) || undefined}
                      cols={rowCols}
                      onToggle={() => toggleRow(path)}
                      onClick={(e) => { if (!selectRow(path, e, order)) rowPickFolder(path) }}
                      onDoubleClick={() => goFolder(path)}
                    />
                    {open && <RowSubtree
                      level={path} depth={1} rowLevel={(lvl) => rowLevel(lvl, treeObjects)} expanded={rowsExpanded}
                      onToggle={toggleRow}
                      onOpenFolder={goFolder} onPickFolder={rowPickFolder} onPickFile={rowPickFile}
                      onOpenFile={(o) => { const lvl = rowLevel(o.key.slice(0, o.key.lastIndexOf('/') + 1), treeObjects).files; openQuickLook({ files: lvl, index: Math.max(0, lvl.findIndex((f) => f.key === o.key)) }) }}
                      selectRow={selectRow} order={order} selectedKeys={rowSelection}
                      onRowContextMenu={(e, payload) => menu.openAt(e, payload)} dragFor={dragFor} dropFilesTo={dropFilesTo}
                      formatDate={formatDate} folderMeta={metaOf} thumbnailFor={thumbOf} cols={rowCols}
                      pickedKey={pickedFolder ?? pickedFile?.key ?? null} currentKey={prefix}
                    />}
                  </Fragment>
                )
              })}
              {rowLevel('', treeObjects).files.map((o) => (
                <FileRow key={o.key} o={o} formatDate={formatDate} thumb={thumbOf(o)} cols={rowCols}
                  selected={rowSelection.has(o.key) || pickedFile?.key === o.key}
                  onDoubleClick={() => { const lvl = rowLevel('', treeObjects).files; openQuickLook({ files: lvl, index: Math.max(0, lvl.findIndex((f) => f.key === o.key)) }) }}
                  onContextMenu={(e) => menu.openAt(e, { type: 'file', path: o.key, o, targets: targetsFor(o.key) })}
                  drag={dragFor?.(o.key)}
                  onClick={(e) => {
                    if (!selectRow(o.key, e, visibleRowOrder(treeObjects))) rowPickFile(o)
                  }} />
              ))}
              </>}
            </ul>
            </div>
            {showRowPreview && previewPane()}
            </div>
            </div>
          )
    )
  }

  /* THE GRID IS THE THIRD VIEW OF ONE SURFACE (user 2026-09-23, reversing the same day's "no pane,
   * no fill height" for the wall: *"why did you not treat this like column and row, with container
   * padding + preview pane? CONSISTENCY"*). Same bordered frame, same fill height, the tiles scroll
   * inside it, the same preview pane beside them, the same count line under it — with the size
   * slider at its right end, Finder's. */
  const tileSize = settings.tileSize ?? 180
  const rowSize = settings.rowSize ?? 'md'
  /* the tile size is the GRID's, not a level's (user 2026-09-24: the slider vanished at the root
   * levels) — one control, drawn wherever the grid is */
  /* THE TREE VIEWS SCALE TOO (user 2026-09-30: *"we might want to scale everything on slider like we
   * do in grid view"*) — in steps of the control ramp, not pixels, so a row is always a control height */
  const RAMP = ['xs', 'sm', 'md', 'lg']
  const rowSlider = (
    <Slider readout="none" size={128} min={0} max={RAMP.length - 1} step={1} value={Math.max(0, RAMP.indexOf(rowSize))}
      onChange={(v) => setSettings({ ...settings, rowSize: RAMP[v] })} aria-label="Row size" />
  )
  const sizeSlider = (
    <Slider readout="none" size={128} min={100} max={360} step={20} value={tileSize}
      onChange={(v) => setSettings({ ...settings, tileSize: v })} aria-label="Tile size" />
  )
  /* `hits` = a query's results: no folders, no root level, each file named by its whole path —
   * in the tree views as a list, the rows' shape; the wall keeps its tiles */
  const wallPane = (files, hits = false) => (
    <div ref={viewRef} className="flex flex-col gap-2" style={{ height: colH }}>
    <div className="relative border rounded flex overflow-hidden flex-1 min-h-0" style={{ borderColor: 'var(--kol-oq-08)' }}>
    {/* THE MARQUEE IS THE PANE'S, not the tile grid's (user 2026-09-23: a drag only started where
      * there were tiles — the grid is as tall as its tiles, the pane is the whole frame). The rows'
      * band has always been on their full-height list; this is the grid catching up. */}
    <div ref={gridMarquee.ref} {...gridMarquee.props}
      className={`relative flex-1 min-w-0 overflow-y-auto p-4${showGridPreview ? ' border-r' : ''}`} style={{ borderColor: 'var(--kol-oq-08)' }}
      /* THE CONTAINER IS A LEVEL, like a column (kol-client-olina 2026-09-23: *"context menu anywhere
       * by right click"*). A tile's own menu stops the event first, so it still wins. */
      onContextMenu={titleLevel ? undefined : (e) => menu.openAt(e, { type: 'level', path: prefix })}
      onClick={(e) => { if (!e.target.closest('[data-marquee-key]')) { setRowSelection(new Set()); setPickedFile(null); setPickedFolder(null); setPickedBucket(null) } }}>
    {gridMarquee.rect && <div className="kol-marquee" style={gridMarquee.rect} />}
    {titleLevel && !hits ? (
      <div className="grid gap-x-3 gap-y-5" style={{ gridTemplateColumns: `repeat(auto-fill, minmax(${tileSize}px, 1fr))` }}>
        {rootItems.map((it) => (
          <MediaTile key={it.key} preview={<Icon name={it.icon} size="100%" className="text-oq-48" />} name={it.name}
            selected={pickedBucket === it.key}
            onClick={(e) => { e.stopPropagation(); setPickedBucket(it.key) }} onDoubleClick={it.open} />
        ))}
      </div>
    ) : <WallBody files={files} layout={hits && !isWall ? 'list' : 'grid'} tiles="media" cardMin={tileSize} onSorted={(list) => { gridOrderRef.current = list }}
      /* THE GRID DRAWS THE LEVEL'S SUBFOLDERS, ahead of the files (same source: kol-client-olina
       * 2026-09-23 — `projects/` read `0 files` over a blank pane). `flat` is the subtree's files
       * and no folders, as it was. */
      folders={settings.flat || smartMatches || hits ? [] : folders.map((f) => prefix + f)}
      {...(hits && { renderName: (o) => o.key })}
      onOpenFolder={goFolder}
      onPickFolder={(path, e) => { if (selectRow(path, e, [...folders.map((f) => prefix + f), ...files.map((f) => f.key)])) return; rowPickFolder(path) }}
      onFolderContextMenu={(e, path) => menu.openAt(e, { type: 'folder', path, targets: targetsFor(path) })}
      onBackgroundClick={() => { setRowSelection(new Set()); setPickedFile(null); setPickedFolder(null); setPickedBucket(null) }}
      sortBy={settings.sortBy} sortDir={settings.sortDir}
      pageSize={settings.pageSize} listId={`${prefix}|${view}|${settings.flat}|${refreshKey}|${bucketMeta.id}`}
      mediaUrl={mediaUrl} downloadUrl={downloadUrl} keySet={keySet} videoPreview={settings.videoPreview} formatDate={formatDate}
      selected={rowSelection} onPick={(o, e, keys) => {
        /* ⇧/⌘ ADD TO THE SELECTION HERE TOO (user 2026-09-23) — the same `selectRow` the rows use;
         * a plain click is still a selection of one and still previews */
        if (selectRow(o.key, e, keys)) return
        setPickedFolder(null); setPickedFile(o)
      }}
      onOpen={(o, files) => openQuickLook({ files, index: Math.max(0, files.findIndex((f) => f.key === o.key)) })}
      /* COPY AND DOWNLOAD LIVE IN THE MENU AND QUICK LOOK (user 2026-09-23) — as in the rows and the
       * columns. The tile carries no buttons. */
      onContextMenu={(e, o) => menu.openAt(e, { type: 'file', path: o.key, o, targets: targetsFor(o.key) })} />}
    </div>
    {showGridPreview && previewPane()}
    </div>
    </div>
  )

  /* The search modal's own ruling (2026-08-01) is that Enter COMMITS the query rather than navigating
   * to whatever happened to be first — so `onExpand` is where a committed query lands, and here
   * that means the top hit. Arrow-then-Enter and a click go through `onSelect`, same jump. */
  const searchResults = rankMedia(objects, searchQuery, { limit: 50 }).map((o) => ({
    id: o.key,
    label: o.displayName ?? o.key.split('/').pop(),
    hint: o.key.slice(0, o.key.lastIndexOf('/') + 1) || 'the bucket root',
    group: KIND_LABEL[kindOf(o)] || 'File',
  }))
  /* THE SEARCH MODAL OPENS ON SOMEWHERE TO GO (user 2026-09-30: it opened blank in olina). */
  const openFolder = (path) => { setAppRoot(false); goFolder(path) }
  const leafOf = (path) => path.replace(/\/$/, '').split('/').pop()
  const hintOf = (path) => dirOf(path) || 'the bucket root'
  const defaultSuggestions = [
    ...Object.keys(folderRows ?? {}).filter((p) => folderRows[p]?.favourite).sort()
      .map((p) => ({ id: p, label: leafOf(p), hint: hintOf(p), group: 'Favourites', icon: 'folder', run: () => openFolder(p) })),
    ...objects.filter((o) => o.favourite && !isSystemFile(o.key))
      .map((o) => ({ id: o.key, label: leafOf(o.key), hint: hintOf(o.key), group: 'Favourites', icon: 'file' })),
    ...smartFolders.map((sf) => ({ id: `smart:${sf.id}`, label: sf.name, group: 'Smart folders', icon: 'folder', run: () => setActiveSmart(sf.id) })),
    ...[...new Set([...treeFolders(''), ...partition(objects.filter((o) => !isSystemFile(o.key)), '').folders])].sort()
      .map((f) => ({ id: f, label: leafOf(f), group: 'Folders', icon: 'folder', run: () => openFolder(f) })),
  ]
  const suggestions = typeof searchSuggestions === 'function' ? searchSuggestions(defaultSuggestions) : (searchSuggestions ?? defaultSuggestions)
  const jumpTo = (key) => {
    const o = objects.find((x) => x.key === key)
    setSearchOpen(false); setSearchQuery('')
    if (!o) return
    setAppRoot(false)
    setPrefix(o.key.slice(0, o.key.lastIndexOf('/') + 1))
    pickFile(o)
  }

  /* A SMART FOLDER IS A SAVED QUERY (media D1 plan v2) — tags it must carry, kinds it may be, text
   * in its path — shown flat, every match wherever it lives, as Finder's smart folders are. */
  const smartMatches = activeSmartFolder ? objects.filter((o) => {
    const q = activeSmartFolder.query ?? {}
    if (isSystemFile(o.key)) return false
    if ((q.tags ?? []).some((t) => !(o.tags ?? []).includes(t))) return false
    if ((q.kinds ?? []).length && !q.kinds.includes(kindOf(o))) return false
    if (q.text && !filterMedia([o], q.text).length) return false
    return true
  }).map((o) => ({ ...o, kind: kindOf(o), poster: posterFor(o.key, keySet), displayKey: o.displayName ?? o.key })) : null
  /* A TYPED QUERY IS A RESULTS LIST (user 2026-09-30, reversing "a flat results list nobody asked
   * for"): pruning the tree to matching paths hid the files one level down and read as "search only
   * finds folders". While the bar holds a query, the tree views show every hit flat, each with its
   * path; clearing it brings the tree back. The wall already was a flat list. */
  const body = (filtered) => (smartMatches
    ? wallPane(filtered ?? smartMatches)
    : filtered && barQuery.trim() ? wallPane(filtered, true)
    : isWall
      ? wallPane(filtered ?? wallFiles)
      : treeBody(filtered ? new Set(filtered.map((f) => f.key)) : null))

  return (
    <div ref={pageRef} {...press} className={`flex flex-col gap-6 ${className}`.trim()}>
    <RowSize.Provider value={rowSize}>
      <LibraryHeader title={title} buckets={buckets} bucketId={bucketMeta.id} appRoot={atTitleRoot} bucketMeta={bucketMeta} writable={writable} headerActions={headerActions} headerTrailing={headerTrailing} onClose={onHeaderClose} onTrash={trash ? () => setTrashOpen(true) : undefined}
        onHome={goTitleRoot}
        onBucket={(v) => { if (v === 'all') goTitleRoot(); else { setAppRoot(false); switchBucket(v) } }}
        onSettings={onOpenSettings ?? (() => setSettingsOpen(true))} />

      {/* THE BANNER SLOT — anything that must sit directly under the header and above the body.
        * The upload drop zone is why it exists: a consumer rendering it AFTER the page put it
        * below an 800px column browser, so pressing Upload scrolled you past the whole browser to
        * reach the target you had just asked for. A slot is the fix; telling the consumer to
        * re-order is not, because the header is ours and the body is ours and there was no
        * between. */}
      {banner}

      <div className="flex flex-col gap-3">
        {/* Breadcrumb (uppercase, active segment at full ink) · folder-view toggle
          *
          * AT 390 THIS ROW FAILED (ColumnBrowserStackMode items 3 + 7, kol-r2b2
          * 2026-09-03, measured on a live site): breadcrumb, the folder-view
          * toggle and the ROW·COLUMN pair share one `justify-between` line with
          * no wrap or collapse rule, so the crumbs wrapped to two lines and
          * `COLUMN` was cut off. Below `md` the crumb becomes ONE middle-elided
          * path line and the toggles collapse into the settings drawer — where
          * both already live, so nothing new was minted to hold them. Both
          * references (iOS Files, Dropbox) put these behind a `···` too. */}
        <div className="flex items-center justify-between gap-4">
          {/* the elided line — first segment, an ellipsis for anything between,
            * and the current one. Below `md` only. */}
          <div className="flex md:hidden items-center gap-2 kol-mono-12 min-w-0">
            <button className={crumbCls(atTitleRoot && !atTop)} onClick={single ? goTitleRoot : goTop}>{ROOT}</button>
            {!atTitleRoot && bucketMeta.id && crumbs.length > 1 && (
              <><span className="text-oq-32">/</span><span className="text-oq-32">…</span></>
            )}
            {!atTitleRoot && bucketMeta.id && (
              <span className="flex items-center gap-2 min-w-0">
                <span className="text-oq-32">/</span>
                <button
                  className={`${crumbCls(true)} truncate`}
                  onClick={() => goFolder(crumbs.length ? crumbs.join('/') + '/' : '')}
                >
                  {(crumbs[crumbs.length - 1] ?? label).toUpperCase()}
                </button>
              </span>
            )}
          </div>
          <div className="hidden md:flex items-center gap-2 kol-mono-12">
            <button className={crumbCls(atTitleRoot && (!atTop || pickedBucket === ROOT_KEY))} onClick={single ? goTitleRoot : goTop}>{ROOT}</button>
            {/* A PICKED BUCKET ENDS THE CRUMB at the title level (user 2026-09-24: a selected folder
              * must show in the crumb, as a selected file does — a bucket is a folder) */}
            {titleLevel && !atTop && pickedBucket && rootItems.some((it) => it.key === pickedBucket) && (
              <span className="flex items-center gap-2"><span className="text-oq-32 select-none">/</span>
                <span className={crumbCls(true)}>{rootItems.find((it) => it.key === pickedBucket).name.toUpperCase()}</span>
              </span>
            )}
            {!atTitleRoot && bucketMeta.id && (
              <span className="flex items-center gap-2">
                <span className="text-oq-32 select-none">/</span>
                {crumbs.length === 0 && !pickedFile && !pickedFolder
                  ? <CopyCrumb className={crumbCls(true)} label={label.toUpperCase()} path={fullPath()} />
                  : <button className={crumbCls(false)} onClick={() => goFolder('')}>{label.toUpperCase()}</button>}
              </span>
            )}
            {!atTitleRoot && crumbs.map((seg, i) => {
              const to = crumbs.slice(0, i + 1).join('/') + '/'
              const last = i === crumbs.length - 1
              const current = last && !pickedFile && !pickedFolder
              return (
                <span key={to} className="flex items-center gap-2">
                  <span className="text-oq-32 select-none">/</span>
                  {current
                    ? <CopyCrumb className={crumbCls(true)} label={seg.toUpperCase()} path={fullPath()} />
                    : <button className={crumbCls(false)} onClick={() => goFolder(to)}>{seg.toUpperCase()}</button>}
                </span>
              )
            })}
            {/* THE PICKED FILE ENDS THE CRUMB IN EVERY VIEW (kol-client-olina 2026-09-23 — it was the
              * columns' alone, so a file picked in rows showed its folder and never itself) */}
            {pickedFile && pickedFile.key.startsWith(prefix) && (
              <span className="flex items-center gap-2"><span className="text-oq-32 select-none">/</span>
                <CopyCrumb className={crumbCls(true)} label={pickedFile.key.slice(prefix.length).toUpperCase()} path={fullPath(pickedFile.key.slice(prefix.length))} />
              </span>
            )}
            {/* …and so does a picked folder */}
            {!pickedFile && pickedFolder && pickedFolder !== prefix && pickedFolder.startsWith(prefix) && (
              <span className="flex items-center gap-2"><span className="text-oq-32 select-none">/</span>
                <CopyCrumb className={crumbCls(true)} label={pickedFolder.slice(prefix.length).replace(/\/$/, '').toUpperCase()} path={fullPath(pickedFolder.slice(prefix.length).replace(/\/$/, ''))} />
              </span>
            )}
          </div>
          {/* the control cluster is desktop-only — below `md` this is the same
            * setting the drawer already carries, and nothing in this row survives 390.
            *
            * ONE SWITCH FOR FOUR VIEWS (the merge, 2026-09-22). Two views were a pair of words;
            * four are an icon strip — the same reason Finder draws four glyphs and no labels.
            * Beside it: the filter bar's funnel and search, which belong to every view now. */}
          <div className="hidden md:flex items-center gap-4">
            {/* FILTER AND SEARCH, ONE WELL (user 2026-09-23): the same chip and height as the view
              * switch beside them — the bare filter frame sat shorter than the well, and search had
              * wandered up to the header. Tooltips are the word, not a sentence. */}
            <ViewToggle variant="icon" viewMode={null} onViewChange={() => {}} options={[
              { value: 'filter', label: 'Filter', icon: 'filter', pressed: filtersOn, onClick: () => setSettings({ ...settings, filters: !filtersOn }) },
              { value: 'search', label: 'Search', icon: 'search', dividerBefore: true, onClick: () => setSearchOpen(true) },
            ]} />
            <Divider variant="vertical" />
            <ViewToggle viewMode={view} onViewChange={setView} variant="icon" options={VIEW_OPTIONS} />
          </div>
        </div>

        {/* PINNED ABOVE THE LIST, below `md` (item 15). Search sits here and not
            inside the ContentFilters wall, because the wall is a DIFFERENT
            SURFACE — on a phone it is a tab away, so a control living in it is
            a control you cannot reach from the thing you are searching. */}
        <div className="flex md:hidden items-center gap-2">
          <SearchInput
            value={query}
            onChange={(e) => setQuery(e.target.value ?? '')}
            onClear={() => setQuery('')}
            placeholder={searchPlaceholder}
            size="sm"
            className="flex-1 min-w-0"
          />
          {/* `···` — view mode and sort, the two the desktop cluster carries and
              item 7 left with nowhere to go. `caret={false}`: an icon trigger is
              already complete, and neither reference draws a chevron on it. */}
          <MenuItem
            label={<Icon name="more" size={16} />}
            caret={false}
            align="end"
            size="sm"
            buttonClassName="shrink-0 px-2"
          >
            {({ close }) => (
              <div className="py-1 w-[200px]">
                {STACK_VIEW_OPTIONS.map((opt) => (
                  <MenuDropdownItem
                    key={opt.value}
                    onClick={() => { setSettings({ ...settings, stackView: opt.value }); close() }}
                    shortcut={(settings.stackView ?? 'list') === opt.value ? <Icon name="check" size={11} /> : undefined}
                  >
                    {opt.label}
                  </MenuDropdownItem>
                ))}
                <MenuDropdownDivider />
                {SORT_OPTIONS.map((opt) => (
                  <MenuDropdownItem
                    key={opt.value}
                    /* tapping the ACTIVE key flips the direction, which is how
                       both references let you reverse without a second control */
                    onClick={() => {
                      const same = (settings.sortBy ?? 'name') === opt.value
                      setSettings({
                        ...settings,
                        sortBy: opt.value,
                        sortDir: same && settings.sortDir !== 'desc' ? 'desc' : 'asc',
                      })
                      close()
                    }}
                    shortcut={(settings.sortBy ?? 'name') === opt.value
                      ? <Icon name={settings.sortDir === 'desc' ? 'arrow-up' : 'arrow-down'} size={11} />
                      : undefined}
                  >
                    {opt.label}
                  </MenuDropdownItem>
                ))}
                {onKinds && (
                  <>
                    <MenuDropdownDivider />
                    <MenuDropdownItem onClick={() => { onKinds(); close() }} shortcut="K">File formats</MenuDropdownItem>
                  </>
                )}
              </div>
            )}
          </MenuItem>
        </div>

        {searchOpen && (
          <CommandOverlay
            open
            onClose={() => { setSearchOpen(false); setSearchQuery('') }}
            query={searchQuery}
            onQueryChange={setSearchQuery}
            placeholder={searchPlaceholder}
            results={searchResults}
            onExpand={() => searchResults[0] && jumpTo(searchResults[0].id)}
            onSelect={(item) => (item.run ? item.run() : jumpTo(item.id))}
            suggestions={suggestions}
            enterLabel="Go to file"
            onOpenResults={(q) => { setSettings({ ...settings, filters: true }); setBarQuery(q) }}
          />
        )}
        {quickLookFolder && (
          <FullscreenOverlay open onClose={() => setQuickLookFolder(null)} scrim>
            {quickLookFolder === ROOT_KEY
              ? previewRoot && <ContainerPreview name={previewRoot.name} icon={previewRoot.icon} facts={previewRoot.facts} width={420} />
              : <FolderPreview path={quickLookFolder} objects={objects} width={420} formatDate={formatDate} />}
          </FullscreenOverlay>
        )}
        {quickLook && (
          <MediaInspector files={quickLook.files} index={quickLook.index} onClose={closeQuickLook} mediaUrl={mediaUrl} downloadUrl={downloadUrl} hasUrl={hasUrl} keySet={keySet} showNav={rowSelection.size > 1}
            onEdit={canEdit ? (f) => { closeQuickLook(); openEditor(f) } : undefined}
            onPrev={() => stepQuickLook(-1)} onNext={() => stepQuickLook(1)} />
        )}
        {editing?.mode === 'edit' && (
          <FileEditorHost o={editing.o} client={client} bucket={bucketKey} onClose={() => setEditing(null)} onSaved={afterEdit} assets={attachables} />
        )}
        {editing?.mode === 'new' && (
          <DocumentEditor mode="new" folder={editing.folder} draft={{ bucket: bucketKey, key: `${editing.folder}.new-document` }} assets={attachables}
            onCreate={createDocument} onClose={() => setEditing(null)} />
        )}
        {settingsOpen && (
          <MediaSettings bucketMeta={bucketMeta} settings={settings} profile={profile} onChange={setSettings} onReset={() => setSettings(null)} onClose={() => setSettingsOpen(false)} settingsFooter={settingsFooter} />
        )}
        {trashOpen && trash && (
          <TrashPanel trash={trash} onClose={() => setTrashOpen(false)} run={runAction} confirm={modal.confirm} />
        )}

        {/* SMART FOLDERS — one chip each; a chip shows its matches flat in place of the body */}
        {canSmart && (smartFolders.length > 0 || canWrite) && !atTitleRoot && (
          <div className="flex flex-wrap items-center gap-2">
            <span className="kol-mono-12 text-fg-48">Smart folders</span>
            {smartFolders.map((sf) => (
              <Tag key={sf.id} variant="secondary" size="sm" hash={false} active={activeSmart === sf.id}
                onClick={() => setActiveSmart((a) => (a === sf.id ? null : sf.id))}
                onRemove={canWrite ? () => deleteSmart(sf) : undefined}>{sf.name}</Tag>
            ))}
            {canWrite && <Button variant="nav" size="xs" iconLeft="plus" onClick={saveSmart}>New smart folder</Button>}
          </div>
        )}
        {/* THE FILTER BAR, above whichever body is mounted (2026-09-22). It filters FILES, so the
          * tree is scoped to the keys it leaves and the wall renders them directly. Off by default
          * — the funnel in the crumb row turns it on. */}
        {filtersOn ? (
          <ContentFilters
            items={filterItemsFav}
            title="Files"
            totalCount={filterItems.length}
            searchKeys={['displayKey']}
            searchValue={barQuery}
            onSearchChange={setBarQuery}
            filterGroups={[{ label: 'Kind', key: 'kind', values: filterKinds }, ...(filterTags.length ? [{ label: 'Tags', key: 'tags', values: filterTags }] : []), ...(anyStarred ? [{ label: 'Favourites', key: 'starred', values: ['favourite'] }] : [])]}
            mutuallyExclusiveFilters={['kind']}
            /* ONE FUNNEL, NOT TWO (2026-09-22): the crumb row's funnel mounts this bar AND opens
             * its panel, and the bar's own funnel closes the bar — the same switch from either
             * end, instead of two controls that each do half of it. */
            filtersOpen
            onFiltersOpenChange={(open) => { if (!open) setSettings({ ...settings, filters: false }) }}
            renderItem={(filtered) => body(filtered)}
          />
        ) : body(null)}

        {/* THE GAPS MATCH ABOVE AND BELOW (user 2026-09-21: "what was unclear about matching the
          * gap above to below"). This carried `mt-2` while the crumb row's icon toggle made that
          * row 8px taller than its text; the toggle was cut 2026-09-22, the crumb row is text
          * height again, and one shared `gap-3` puts both gaps level on its own. */}
        {/* THE COUNT LINE IS EVERY VIEW'S (user 2026-09-23) — and in the grid it carries the tile-size
          * slider at its right end, bare: no glyph, no readout (Finder's). */}
        {atTitleRoot && !single ? (
          <div className="flex items-center justify-between gap-4 h-4">
          <p className="kol-mono-12 text-fg-48" hidden={!showCount}>
            {atTop ? '1 folder' : `${bucketList.length} ${bucketList.length === 1 ? 'bucket' : 'buckets'}`}
            {folderTree && ` · ${Object.values(folderTree).reduce((n, t) => n + (t.files ?? 0), 0)} files · ${formatSize(Object.values(folderTree).reduce((n, t) => n + (t.bytes ?? 0), 0))}`}
          </p>
          {isWall ? sizeSlider : !phone && rowSlider}
          </div>
        ) : isWall ? (
          /* ONE LINE'S HEIGHT, like the other views' count line — the slider is 24px tall and made
           * this row 8px taller than the fill budget, so the page scrolled by that much */
          <div className="flex items-center justify-between gap-4 h-4">
            <p className="kol-mono-12 text-fg-48" hidden={!showCount}>
              {!settings.flat && folders.length > 0 && `${folders.length} folder${folders.length > 1 ? 's' : ''} · `}{wallFiles.length} {wallFiles.length === 1 ? 'file' : 'files'} · {formatSize(wallFiles.reduce((n, o) => n + (o.size ?? 0), 0))}
            </p>
            {sizeSlider}
          </div>
        ) : showCount && (
          <p className="kol-mono-12 text-fg-48">
            {folders.length > 0 && `${folders.length} folder${folders.length > 1 ? 's' : ''} · `}
            {rawFiles.length} {rawFiles.length === 1 ? 'file' : 'files'} · {formatSize(totalBytes)}
            {/* THE GREY TAIL TOTALS WHERE YOU STAND (kol-client-olina 2026-09-23): the bucket at the
              * root, the current folder recursively below it. Hidden when it would repeat the
              * bright part — a leaf has nothing beneath it to add. */}
            {!prefix && rawFiles.length !== bucketFiles && <span className="text-fg-32">{'  ·  bucket: '}{bucketFiles} files · {formatSize(bucketBytes)}</span>}
            {prefix && scoped.length !== levelFiles.length && <span className="text-fg-32">{`  ·  in ${crumbs.at(-1)}: `}{scoped.length} files · {formatSize(scoped.reduce((n, o) => n + (o.size || 0), 0))}</span>}
            {systemCount > 0 && <span className="text-fg-32">{'  ·  '}{systemCount} system files hidden</span>}
          </p>
        )}
        {void onOpen}

        {/* ONE MENU FOR EVERY ROW. `useContextMenu` carries the payload from whichever row opened
          * it, so this is a single instance rather than one per row, and the entries are built from
          * what `fileActions` actually supplies. IT MOUNTS ON A READ-ONLY BUCKET TOO (kol-website,
          * 2026-10-08: on B2 "I cant right click to get context modal … cant download assets"): Copy URL
          * and Download are read verbs — the client's URLs — and gating the whole menu on `canWrite`
          * took them away with the write verbs. `fa` is the write seam: empty when the bucket cannot
          * be written, so every write verb below folds away and the read verbs stay. */}
        {(() => { const fa = canWrite ? fileActions : {}; return (
          <ContextMenu menu={menu}>
            {(target) => {
              if (!target) return null
              const isFolder = target.type === 'folder'
              const isLevel = target.type === 'level'
              /* A SELECTION OF MORE THAN ONE takes over the verbs that can act on a set. Rename is
               * not one of them — renaming five things to one name is not a thing — so it stays
               * single and disappears from a multi-selection rather than lying about what it does. */
              const many = (target.targets ?? []).length > 1 ? target.targets : null
              const custom = (fa.items ?? []).filter((it) => !it.when || it.when(target))
              const customItems = custom.length > 0 && (
                <>
                  <MenuDropdownDivider />
                  {custom.map((it) => (
                    <MenuDropdownItem key={it.label} iconLeft={it.icon ? <Icon name={it.icon} size={14} /> : undefined} onClick={() => runAction(() => it.run(target))}>{it.label}</MenuDropdownItem>
                  ))}
                </>
              )
              if (many) {
                return (
                  <>
                    <MenuDropdownItem disabled>{many.length} selected</MenuDropdownItem>
                    <MenuDropdownDivider />
                    {fa.move && (
                      <MenuDropdownItem iconLeft={<Icon name="arrow-right" size={14} />} onClick={() => doBatch(many, 'move', 'Move')}>Move {many.length} to…</MenuDropdownItem>
                    )}
                    {hasUrl && many.every((p) => !p.endsWith('/')) && (
                      <>
                        <MenuDropdownItem iconLeft={<Icon name="copy" size={14} />} onClick={() => doCopyUrl(many)}>Copy {many.length} URLs</MenuDropdownItem>
                        <MenuDropdownItem iconLeft={<Icon name="download" size={14} />} onClick={() => doDownload(many)}>Download {many.length}</MenuDropdownItem>
                        {canTag && <MenuDropdownItem iconLeft={<Icon name="hash-01" size={14} />} onClick={() => doTags(many)}>Add tags to {many.length}…</MenuDropdownItem>}
                      </>
                    )}
                    {fa.remove && (
                      <MenuDropdownItem iconLeft={<Icon name="trash" size={14} />} onClick={() => doBatch(many, 'remove', 'Delete')}>Delete {many.length}</MenuDropdownItem>
                    )}
                    {customItems}
                  </>
                )
              }
              const qlFile = onPickFile && target.type === 'file' ? objects.find((o) => o.key === target.path) : null
              const insFile = onInsert && target.type === 'file' ? (objects.find((o) => o.key === target.path) ?? target.o) : null
              return (
                <>
                  {insFile && (!canInsert || canInsert(insFile)) && (
                    <>
                      <MenuDropdownItem iconLeft={<Icon name="plus" size={14} />} onClick={() => { onInsert(insFile); close() }}>Insert</MenuDropdownItem>
                      <MenuDropdownDivider />
                    </>
                  )}
                  {/* A PICKER'S TAP SELECTS (`tapSelects`), so Quick Look needs a door of its own on a
                    * phone — the menu (kol-fxr 2026-10-08). Only with `onPickFile`; others are unchanged. */}
                  {qlFile && (
                    <>
                      <MenuDropdownItem iconLeft={<Icon name="eye-on" size={14} />} onClick={() => openQuickLook({ files: [qlFile], index: 0 })}>Quick Look</MenuDropdownItem>
                      <MenuDropdownDivider />
                    </>
                  )}
                  {fa.createFolder && (
                    <MenuDropdownItem iconLeft={<Icon name="folder" size={14} />} onClick={() => doCreateFolder(isFolder ? target.path : (isLevel ? target.path : prefix))}>
                      New folder{isFolder ? ` in ${target.path.replace(/\/$/, '').split('/').pop()}` : ''}
                    </MenuDropdownItem>
                  )}
                  {/* NEW DOCUMENT is the writing page (a name, a type, fields for markdown); New file stays for
                      a client that cannot write text — it makes the empty file and nothing else */}
                  {canNewDoc ? (
                    <MenuDropdownItem iconLeft={<Icon name="file" size={14} />} onClick={() => newDocument(isFolder ? target.path : (isLevel ? target.path : prefix))}>
                      New document…
                    </MenuDropdownItem>
                  ) : fa.createFile && (
                    <MenuDropdownItem iconLeft={<Icon name="file" size={14} />} onClick={() => doCreateFile(isFolder ? target.path : (isLevel ? target.path : prefix))}>
                      New file
                    </MenuDropdownItem>
                  )}
                  {dropFilesTo && !(target.type === 'file') && (
                    <MenuDropdownItem iconLeft={<Icon name="upload" size={14} />} onClick={() => doUpload(target.path)}>
                      {isFolder ? `Upload into ${target.path.replace(/\/$/, '').split('/').pop()}…` : 'Upload here…'}
                    </MenuDropdownItem>
                  )}
                  {!isLevel && <MenuDropdownDivider />}
                  {!isLevel && fa.rename && (
                    <MenuDropdownItem iconLeft={<Icon name="edit" size={14} />} onClick={() => doRename(target.path, isFolder)}>Rename</MenuDropdownItem>
                  )}
                  {!isLevel && fa.move && (
                    <MenuDropdownItem iconLeft={<Icon name="arrow-right" size={14} />} onClick={() => doMove(target.path)}>Move to…</MenuDropdownItem>
                  )}
                  {/* A FOLDER GETS COPY URL TOO (user 2026-09-23) — through the SAME `mediaUrl`
                      seam a file goes through, so what a folder URL means is the consumer's call
                      (a CDN prefix, a listing, a signed path) and never this page's guess. No
                      Download: a folder is not an object to fetch. */}
                  {hasUrl && !isLevel && (
                    <MenuDropdownItem iconLeft={<Icon name="copy" size={14} />} onClick={() => doCopyUrl([target.path])}>Copy URL</MenuDropdownItem>
                  )}
                  {hasUrl && target.type === 'file' && (
                    <MenuDropdownItem iconLeft={<Icon name="download" size={14} />} onClick={() => doDownload([target.path])}>Download</MenuDropdownItem>
                  )}
                  {!isLevel && canTag && (
                    <MenuDropdownItem iconLeft={<Icon name="hash-01" size={14} />} onClick={() => doTags([target.path])}>Tags…</MenuDropdownItem>
                  )}
                  {!isLevel && canFavourite && (
                    <MenuDropdownItem iconLeft={<Icon name={isFavourite(target.path) ? 'star-solid' : 'star'} size={14} />} onClick={() => toggleFavourite(target.path)}>
                      {isFavourite(target.path) ? 'Remove from favourites' : 'Add to favourites'}
                    </MenuDropdownItem>
                  )}
                  {target.type === 'file' && canEdit && editKindOf(target.o ?? { key: target.path }) && (
                    <MenuDropdownItem iconLeft={<Icon name="edit" size={14} />} onClick={() => openEditor(objects.find((o) => o.key === target.path) ?? { key: target.path })}>Edit</MenuDropdownItem>
                  )}
                  {!isLevel && fa.remove && (
                    <>
                      <MenuDropdownDivider />
                      <MenuDropdownItem iconLeft={<Icon name="trash" size={14} />} onClick={() => doDelete(target.path, isFolder)}>Delete</MenuDropdownItem>
                    </>
                  )}
                  {customItems}
                </>
              )
            }}
          </ContextMenu>
        ) })()}
        {busyAction && <span className="sr-only" role="status">Working…</span>}

        {/* THE TAB PILL (item 16) — floats over the list, so the list owes it
            room or its last row sits under the bar forever.
            THE SPACER GOES LAST, AND THAT IS THE WHOLE POINT
            (TabBarSpacerAboveTheList, kol-r2b2 2026-09-04). `MobileTabBar` is
            `fixed`, so where it sits in the tree is irrelevant — the SPACER is
            in NORMAL FLOW, so its position is everything. Rendered before the
            list it failed twice at once: a 56px hole punched into the gap under
            the pinned search, and the last row still running 99px under the
            bar. The comment above named the failure it was meant to prevent and
            the block sat in the wrong place anyway. Below `md` only; above it
            the 2026-08-26 one-view ruling stands. */}
        {tabs?.length > 0 && (
          <>
            <div className="md:hidden" aria-hidden="true" style={{ height: TABBAR_H }} />
            <TabsRow look="stacked" placement="phone" tabs={tabs} value={activeTab} onChange={onTabChange} />
          </>
        )}
      </div>
    </RowSize.Provider>
    </div>
  )
}


/* ── THE WALL, as a body ────────────────────────────────────────────────────
 * The grid / list of files, its paging and its inspector, lifted out of
 * `MediaLibraryLibrary` so ONE surface can render it beside the folder views
 * (the merge, user 2026-09-22: "they both just display files"). The page above
 * it owns the listing, the filtering and the selection; this owns the tiles.
 *
 * `renderName` / `renderActions` are seams because the standalone wall edits a
 * name in place and the merged surface does not — the markup is the same either
 * way, which is the whole reason this is one component and not two.
 */
/* The kinds that ARE a document — the ones whose page is worth zooming into a tile. */
const DOC_KINDS = new Set(['markdown', 'json', 'yaml', 'text', 'code'])

/* THE TILE MEASURES ITSELF (user 2026-09-23: *"is nothing about the thumbnails responsive?"*).
 * A document in a tile is zoomed, and the zoom was a fixed step — so the same file read one size
 * in a 260px card and another in a 400px one, because `zoom` reflows the page: at zoom z a
 * `width: 100%` page lays out at box/z CSS pixels. Solving for a CONSTANT page width keeps the
 * text the same physical size at every tile size: z = box / PAGE. That number cannot be derived
 * in CSS from the box, which is why this measures.
 *
 * `PAGE` is the width the page is laid out AT, not a paper size: 720 is where the doc page's
 * 24px padding, prose measure and code blocks read as a page rather than as a column of wrap. */
/* CODE IS NOT PROSE, so it is not laid out on a prose page (user 2026-09-23: *"code is small here
 * in the container"*). A narrower target page means a bigger zoom for the same box, which is what
 * makes 60-column code readable in a tile where a 720px page turned it into texture. */
const DOC_PAGE_W = 720
const CODE_PAGE_W = 460
const pageWidthFor = (kind) => (kind === 'markdown' || kind === 'text' ? DOC_PAGE_W : CODE_PAGE_W)
const DOC_ZOOM_MIN = 0.18
const DOC_ZOOM_MAX = 0.6

function DocThumb({ children, className, onClick, pageWidth = DOC_PAGE_W }) {
  const box = useRef(null)
  const [zoom, setZoom] = useState(null)
  useEffect(() => {
    const el = box.current
    if (!el || typeof ResizeObserver === 'undefined') return undefined
    const measure = () => {
      const w = el.getBoundingClientRect().width
      if (!w) return
      setZoom(Math.min(DOC_ZOOM_MAX, Math.max(DOC_ZOOM_MIN, Math.round((w / pageWidth) * 100) / 100)))
    }
    measure()
    const ro = new ResizeObserver(measure)
    ro.observe(el)
    return () => ro.disconnect()
  }, [pageWidth])
  return (
    <div ref={box} className={className} onClick={onClick} style={zoom ? { '--kol-thumb-zoom': zoom } : undefined}>
      {children}
    </div>
  )
}

export function WallBody({
  files, layout = 'list', sortBy = 'name', sortDir = 'asc', pageSize = 200, listId = '', cardMin = 260,
  mediaUrl, downloadUrl, keySet, videoPreview, formatDate = defaultFormatDate,
  selected, selectMode = false, onToggleSelect, onSorted, onContextMenu, onPick,
  /* the media grid's folder tiles — see the page's `wallPane` */
  folders = [], onOpenFolder, onPickFolder, onFolderContextMenu,
  /* ONE INSPECTOR (user 2026-09-23). Given `onOpen`, the wall hands the open UP and keeps no
   * lightbox of its own — otherwise a page that also opens one (space, a row double-click) ends up
   * with two stacked overlays of the same file. The standalone wall passes nothing and keeps its
   * own, exactly as it was. */
  onOpen,
  renderName = (o) => o.displayKey?.split('/').pop() ?? o.key.split('/').pop(),
  renderActions,
  /* `tiles="media"` (2026-09-23) — the file manager's grid: `MediaTile`, preview over name, no card
   * and no buttons (the verbs live in the right-click menu and Quick Look). `card` is the site
   * wall's `ContentCard` and stays the default, so an embedded wall renders exactly as before. */
  tiles = 'card',
  /* the grid's drag-select band and its empty-space click — both the page's, like the rows' */
  marquee, onBackgroundClick,
}) {
  const PAGE = pageSize || Infinity
  const [visible, setVisible] = useState(PAGE)
  const [lastListId, setLastListId] = useState(listId)
  const [lightboxIndex, setLightboxIndex] = useState(null)
  if (listId !== lastListId) { setLastListId(listId); setVisible(PAGE) }
  if (layout === 'off') return null

  /* ONE PREVIEWER — `KindPreview`, as the column pane uses. An image is the caller's <img>, a
   * video keeps its sibling poster when the bucket has one, and every other kind falls through. */
  const renderThumb = (o, onClick) => {
    const kind = kindOf(o)
    const poster = (isVideo(o.contentType) || kind === 'playlist') ? posterFor(o.key, keySet) : null
    const imgSrc = isImage(o.contentType) ? mediaUrl(o.key) : poster && videoPreview !== 'none' ? mediaUrl(poster) : null
    if (imgSrc) return <img src={imgSrc} alt="" loading="lazy" className={onClick ? 'cursor-zoom-in' : undefined} onClick={onClick || undefined} />
    if (isVideo(o.contentType) && videoPreview === 'none') {
      return (
        <div className={`w-full h-full flex items-center justify-center bg-oq-04 overflow-hidden${onClick ? ' cursor-zoom-in' : ''}`} onClick={onClick || undefined}>
          <span className="kol-mono-12 text-fg-48">video</span>
        </div>
      )
    }
    /* ONE LADDER (user 2026-09-23). Every kind that is not the caller's own image goes through
     * `KindPreview` at `fit="tile"` — audio gets the same tile the pane shows, a PDF its first
     * page, a document its zoomed page. The glyph branch that stood here was a second ladder, and
     * a second ladder is the thing this pass exists to delete: a glyph is for a FOLDER. */
    const cls = `kol-media-thumb w-full h-full flex items-center justify-center bg-oq-04 overflow-hidden${onClick ? ' cursor-zoom-in' : ''}`
    const preview = <KindPreview o={o} fit="tile" urlOf={(x) => mediaUrl(x.key)} poster={poster ? mediaUrl(poster) : undefined} kindOf={kindOf} kindLabel={KIND_LABEL} />
    if (DOC_KINDS.has(kind)) {
      return <DocThumb className={cls} pageWidth={pageWidthFor(kind)} onClick={onClick || undefined}>{preview}</DocThumb>
    }
    return <div className={cls} onClick={onClick || undefined}>{preview}</div>
  }

  const sorted = sortFiles(files, sortBy, sortDir)
  onSorted?.(sorted)
  const shown = sorted.slice(0, visible)
  const more = sorted.length - shown.length
  const isSelected = (key) => !!selected?.has(key)
  const openAt = (o, idx) => (onOpen ? onOpen(o, sorted) : setLightboxIndex(idx))
  return (
    <div className="flex flex-col gap-3">
      {sorted.length === 0 && !(layout === 'grid' && tiles === 'media' && folders.length) ? null : layout !== 'grid' ? (
        <div className="flex flex-col">
          {shown.map((o, idx) => (
            <ContentRow key={o.key} variant="default" media={renderThumb(o, null)} title={renderName(o)} date={formatDate(o.uploaded)} size={formatSize(o.size)}
              actions={renderActions?.(o, true)} selected={isSelected(o.key)}
              onContextMenu={onContextMenu ? (e) => onContextMenu(e, o) : undefined}
              onDoubleClick={() => openAt(o, idx)}
              onClick={selectMode ? (e) => onToggleSelect?.(idx, o.key, e.shiftKey) : onPick ? (e) => onPick(o, e, sorted.map((f) => f.key)) : undefined} />
          ))}
        </div>
      ) : tiles === 'media' ? (
        <div ref={marquee?.ref} {...marquee?.props} className="relative grid gap-x-3 gap-y-5"
          style={{ gridTemplateColumns: `repeat(auto-fill, minmax(${cardMin}px, 1fr))` }}
          onClick={(e) => { if (!e.target.closest('[data-marquee-key]')) onBackgroundClick?.() }}>
          {marquee?.rect && <div className="kol-marquee" style={marquee.rect} />}
          {folders.map((path) => (
            <MediaTile key={path} markKey={path} preview={<Icon name="folder" size="100%" className="text-oq-48" />}
              name={path.replace(/\/$/, '').split('/').pop()} selected={isSelected(path)}
              onContextMenu={onFolderContextMenu ? (e) => onFolderContextMenu(e, path) : undefined}
              onDoubleClick={() => onOpenFolder?.(path)}
              onClick={onPickFolder ? (e) => onPickFolder(path, e) : undefined} />
          ))}
          {shown.map((o, idx) => (
            <MediaTile key={o.key} markKey={o.key} preview={renderThumb(o, null)} name={renderName(o)} selected={isSelected(o.key)}
              onContextMenu={onContextMenu ? (e) => onContextMenu(e, o) : undefined}
              onDoubleClick={() => openAt(o, idx)}
              onClick={onPick ? (e) => onPick(o, e, sorted.map((f) => f.key)) : undefined} />
          ))}
        </div>
      ) : (
        <div className="grid gap-3" style={{ gridTemplateColumns: `repeat(auto-fill, minmax(${cardMin}px, 1fr))` }}>
          {shown.map((o, idx) => (
            <ContentCard key={o.key} variant="default" media={renderThumb(o, null)}
              control={<ActionButton chrome="media" icon="download" confirmIcon="check" label="Download" confirmLabel="Downloaded" href={downloadUrl(o.key)} />}
              controlStart={selectMode ? <ToggleCheckbox variant="media" checked={isSelected(o.key)} onChange={() => onToggleSelect?.(idx, o.key, false)} onClick={(e) => e.stopPropagation()} aria-label={`Select ${o.key}`} /> : undefined}
              title={renderName(o)} date={formatDate(o.uploaded)} size={<SizeOrDownload href={downloadUrl(o.key)}>{formatSize(o.size)}</SizeOrDownload>}
              actions={renderActions?.(o)} selected={isSelected(o.key)}
              onContextMenu={onContextMenu ? (e) => onContextMenu(e, o) : undefined}
              /* ONE GESTURE SET ACROSS THE THREE VIEWS (2026-09-23): click selects, double-click
               * opens, right-click is the menu. The media used to open on a single click, which
               * made the grid the one view where a click did something else. */
              onDoubleClick={() => openAt(o, idx)}
              onClick={selectMode ? (e) => onToggleSelect?.(idx, o.key, e.shiftKey) : onPick ? (e) => onPick(o, e, sorted.map((f) => f.key)) : undefined} />
          ))}
        </div>
      )}
      {more > 0 && (
        <button type="button" onClick={() => setVisible((v) => v + PAGE)} className="kol-mono-12 text-fg-48 hover:text-fg-default transition-colors self-start py-2">
          Show {Math.min(more, PAGE)} more · {more} remaining
        </button>
      )}
      {lightboxIndex !== null && !onOpen && (
        <MediaInspector files={sorted} index={lightboxIndex} onClose={() => setLightboxIndex(null)} mediaUrl={mediaUrl} downloadUrl={downloadUrl} keySet={keySet}
          onPrev={() => setLightboxIndex((i) => (i - 1 + sorted.length) % sorted.length)} onNext={() => setLightboxIndex((i) => (i + 1) % sorted.length)} />
      )}
    </div>
  )
}

/* ══ LIBRARY — the content-filters wall ═══════════════════════════════════ */
export function MediaLibraryLibrary({
  client, title = 'MEDIA', bucket, onBucketChange, prefix = '', defaults, settings: settingsProp, onSettingsChange,
  headerActions, headerTrailing, refreshKey, header = true, stats = true, settingsFooter, className = '', banner,
  /* the same pill the browse page takes — the wall is one of the surfaces it
     switches between, so it has to carry it too (item 16) */
  tabs, activeTab, onTabChange,
  /* A SEAM ON ONE PAGE IS A SEAM ON BOTH, when the field is one both render
   * (FormatDateSkipsTheLibraryPage, kol-r2b2 2026-09-04). This reached
   * `MediaLibraryBrowse` and not here, so a consumer passing ONE props object
   * to both got `19.6.2026` on the Browse tab and `2026-06-19` on the Files tab
   * — two formats for one field, one tap apart. The desktop stack had the same
   * mismatch; the tab pill just moved the two halves close enough to see it.
   *
   * THE RULE, WIDENED — the fourth defect of this exact shape (`settingsFooter`
   * documented and hardcoded past · `thumbnailFor` and `folderMeta` not
   * forwarded · now this). These two pages are ONE SURFACE SPLIT IN TWO and a
   * consumer hands the same props object to both, so: a prop naming how a
   * SHARED FIELD renders is added to BOTH pages in the same edit. Props about a
   * concept only one page has stay put — `thumbnailFor`, `folderMeta` and
   * `stackView` are folder and stack concepts and are correctly absent here. */
  formatDate = defaultFormatDate,
}) {
  const [ownBucket, setOwnBucket] = useState(bucket)
  const bucketId = bucket ?? ownBucket
  const lib = useBucketLibrary({ client, bucket: bucketId, defaults, settings: settingsProp, onSettingsChange, refreshKey })
  const { buckets, bucketMeta, settings, setSettings, objects, setObjects, error, mediaUrl, downloadUrl, writable } = lib
  /* the same one dialog idiom as the browse page — see its note on `useModal()` */
  const modal = useModal()
  const [settingsOpen, setSettingsOpen] = useState(false)
  const [editingKey, setEditingKey] = useState(null)
  const [editingValue, setEditingValue] = useState('')
  const [renaming, setRenaming] = useState(false)
  const [selectMode, setSelectMode] = useState(false)
  const [selected, setSelected] = useState(() => new Set())
  const [lastIdx, setLastIdx] = useState(null)
  const [busy, setBusy] = useState(false)
  const sortedRef = useRef([])
  /* the wall is FLAT by default — the folders are the browse page's */
  const { flat = true, layout, sortBy, sortDir, pageSize, videoPreview } = { flat: true, ...settings }
  const kinds = new Set(settings.kinds)
  const setFlat = (v) => setSettings({ ...settings, flat: v })
  const setLayout = (v) => setSettings({ ...settings, layout: v })
  const listId = `${prefix}|${flat}|${[...kinds].sort().join(',')}|${pageSize}|${refreshKey}|${bucketMeta.id}`
  const handleSort = (field) => setSettings(sortBy === field ? { ...settings, sortDir: sortDir === 'asc' ? 'desc' : 'asc' } : { ...settings, sortBy: field, sortDir: 'asc' })

  useEffect(() => {
    if (!selectMode) return undefined
    const onKey = (e) => { if (e.key === 'Escape') { setSelectMode(false); setSelected(new Set()); setLastIdx(null) } }
    window.addEventListener('keydown', onKey)
    return () => window.removeEventListener('keydown', onKey)
  }, [selectMode])

  if (error) return <p className="kol-mono-12 text-ui-error">Error: {error}</p>

  const scoped = prefix ? objects.filter((o) => o.key.startsWith(prefix)) : objects
  const { files: dirFiles } = partition(scoped, prefix)
  const flatFiles = scoped.map((o) => ({ ...o, displayKey: o.displayName ?? (prefix ? o.key.slice(prefix.length) : o.key) }))
  const levelFiles = flat ? flatFiles : dirFiles
  const systemCount = levelFiles.filter((o) => isSystemFile(o.key)).length
  const visibleFiles = kinds.has('system') ? levelFiles : levelFiles.filter((o) => !isSystemFile(o.key))
  const keySet = new Set(objects.map((o) => o.key))
  const grouped = settings.groupVariants ? groupVariants(visibleFiles) : visibleFiles
  const rawFiles = (settings.foldSegments ? groupSegments(grouped) : grouped).map((o) => ({ ...o, kind: kindOf(o), poster: posterFor(o.key, keySet) }))
  const profile = profileOf(objects, rawFiles, systemCount)
  const presentKinds = Object.keys(profile.kinds).sort()
  const files = rawFiles.filter((o) => kinds.has(o.kind))
  const chipKinds = presentKinds.filter((k) => kinds.has(k))
  const totalBytes = rawFiles.reduce((n, o) => n + (o.totalSize ?? o.size ?? 0), 0)

  const handleCopy = (key) => navigator.clipboard.writeText(mediaUrl(key))
  const handleDelete = async (key) => {
    if (!(await modal.confirm(`Delete "${key}"? This cannot be undone.`, { okLabel: 'Delete' }))) return
    try { await client.deleteObject(key); setObjects((prev) => prev.filter((o) => o.key !== key)) } catch (e) { await modal.alert(`Delete failed: ${e.message}`) }
  }
  const startRename = (key) => { setEditingKey(key); setEditingValue(key) }
  const cancelRename = () => { setEditingKey(null); setEditingValue('') }
  const commitRename = async () => {
    const from = editingKey; const to = editingValue.trim()
    if (!from || !to || from === to) { cancelRename(); return }
    setRenaming(true)
    try { const r = await client.renameObject(from, to); const newKey = r?.to || to; setObjects((prev) => prev.map((o) => (o.key === from ? { ...o, key: newKey } : o))); setEditingKey(null); setEditingValue('') }
    catch (e) { await modal.alert(`Rename failed: ${e.message}`) } finally { setRenaming(false) }
  }
  const toggleSelect = (idx, key, shift) => {
    setSelected((prev) => {
      const next = new Set(prev)
      if (shift && lastIdx !== null) { const [a, b] = [Math.min(lastIdx, idx), Math.max(lastIdx, idx)]; for (let i = a; i <= b; i++) next.add(sortedRef.current[i].key) }
      else if (next.has(key)) next.delete(key)
      else next.add(key)
      return next
    })
    setLastIdx(idx)
  }
  const selectAll = () => setSelected(new Set(sortedRef.current.map((o) => o.key)))
  const exitSelect = () => { setSelectMode(false); setSelected(new Set()); setLastIdx(null) }
  const batchMove = async () => {
    const folder = await modal.prompt(`Move ${selected.size} file(s) into folder (under ${prefix || 'root'}):`, '', { okLabel: 'Move' })
    if (folder == null) return
    const clean = folder.replace(/^\/+|\/+$/g, '').trim()
    if (!clean) return
    setBusy(true)
    const remap = new Map(); const failures = []
    for (const from of selected) {
      const to = moveKey(from, prefix, clean)
      if (from === to) continue
      try { const r = await client.renameObject(from, to); remap.set(from, r?.to || to) } catch (e) { failures.push(`${from.split('/').pop()}: ${e.message}`) }
    }
    setObjects((prev) => prev.map((o) => (remap.has(o.key) ? { ...o, key: remap.get(o.key) } : o)))
    setBusy(false); exitSelect()
    if (failures.length) await modal.alert(`Moved ${remap.size}. ${failures.length} failed:\n${failures.join('\n')}`)
  }
  const batchDelete = async () => {
    if (!(await modal.confirm(`Delete ${selected.size} file(s)? This cannot be undone.`, { okLabel: 'Delete' }))) return
    setBusy(true)
    const done = []; const failures = []
    for (const key of selected) { try { await client.deleteObject(key); done.push(key) } catch (e) { failures.push(`${key.split('/').pop()}: ${e.message}`) } }
    setObjects((prev) => prev.filter((o) => !done.includes(o.key)))
    setBusy(false); exitSelect()
    if (failures.length) await modal.alert(`Deleted ${done.length}. ${failures.length} failed:\n${failures.join('\n')}`)
  }
  const batchDownload = () => {
    for (const key of selected) { const a = document.createElement('a'); a.href = downloadUrl(key); a.download = key.split('/').pop(); document.body.appendChild(a); a.click(); a.remove() }
  }

  const renderActions = (o, row = false) => {
    if (editingKey === o.key) {
      return (
        <div className="flex gap-2">
          <Button tone="primary" size="sm" onClick={commitRename} disabled={renaming}>{renaming ? 'Saving…' : 'Save'}</Button>
          <Button tone="ghost" size="sm" onClick={cancelRename} disabled={renaming}>Cancel</Button>
        </div>
      )
    }
    return (
      <div className={row ? 'flex translate-y-[2px] items-center gap-2' : 'flex h-full flex-col items-center justify-between'}>
        {!selectMode && (
          <>
            <ActionButton chrome="inline" size="sm" icon="copy" confirmIcon="check" label="Copy URL" confirmLabel="Copied" onAction={() => handleCopy(o.key)} />
            {writable && !row && <span className="invisible" aria-hidden><ActionButton chrome="inline" size="sm" icon="trash" label="" /></span>}
          </>
        )}
        {writable && selectMode && (
          <>
            <ActionButton chrome="inline" size="sm" icon="edit" label="Rename" onAction={() => startRename(o.key)} />
            <ActionButton chrome="inline" size="sm" icon="trash" confirmIcon="check" label="Delete" confirmLabel="Deleted" onAction={() => handleDelete(o.key)} />
          </>
        )}
      </div>
    )
  }
  const renderNameCell = (o) => {
    if (editingKey === o.key) {
      return <Input size="sm" width="100%" value={editingValue} autoFocus disabled={renaming} onChange={(e) => setEditingValue(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') commitRename(); if (e.key === 'Escape') cancelRename() }} />
    }
    return o.displayKey.split('/').pop()
  }
  const layoutOptions = [
    ...(writable ? [{ value: 'grid', label: selectMode ? 'CANCEL' : 'SELECT', active: !selectMode, title: 'Select multiple files', onClick: () => (selectMode ? exitSelect() : setSelectMode(true)) }] : []),
    { value: 'list', label: flat ? 'TREE' : 'FLAT', active: flat, title: 'Show all files recursively', onClick: () => setFlat(!flat) },
  ]

  return (
    <div className={`flex flex-col gap-6 ${className}`.trim()}>
      {header && (
        <LibraryHeader title={title} buckets={buckets} bucketId={bucketMeta.id} appRoot={false} bucketMeta={bucketMeta} writable={writable} headerActions={headerActions} headerTrailing={headerTrailing}
          onBucket={(v) => { if (v === 'all') return; if (bucket == null) setOwnBucket(v); onBucketChange?.(v) }} onSettings={() => setSettingsOpen(true)} />
      )}
      {/* A SEAM ON ONE PAGE IS A SEAM ON BOTH — `banner` is shared chrome, so it lands here in the
        * same edit it landed on the browse page. See the note on `formatDate` above. */}
      {banner}
      {settingsOpen && (
        <MediaSettings bucketMeta={bucketMeta} settings={settings} profile={profile} onChange={setSettings} onReset={() => setSettings(null)} onClose={() => setSettingsOpen(false)} settingsFooter={settingsFooter} />
      )}

      {/* `stats={false}` (ColumnBrowserSeams, kol-r2b2 2026-08-28) — beside `header={false}`: a browse
        * page stacked above this one already prints the folder-aware count ("it used to be a row") */}
      {stats && (
        <p className="kol-mono-12 text-fg-48">
          {files.length !== rawFiles.length ? `${files.length} of ${rawFiles.length}` : rawFiles.length}{' '}{rawFiles.length === 1 ? 'file' : 'files'} · {formatSize(totalBytes)}
          {systemCount > 0 && <span className="text-fg-32">{'  ·  '}{systemCount} system files hidden</span>}
        </p>
      )}

      <ContentFilters
        items={files}
        title="Files"
        totalCount={files.length}
        searchKeys={['displayKey']}
        filterGroups={[{ label: 'Kind', key: 'kind', values: chipKinds }]}
        mutuallyExclusiveFilters={['kind']}
        leadingActions={writable ? (
          <div className={`flex items-center gap-4 kol-mono-12 text-fg-48 h-8 ${selectMode ? '' : 'hidden'}`} aria-hidden={!selectMode}>
            <span className="text-fg-default">{selected.size} selected</span>
            <button type="button" onClick={selectAll} className="hover:text-fg-default transition-colors">Select all ({files.length})</button>
            <Divider variant="vertical" />
            <button type="button" disabled={!selected.size || busy} onClick={batchMove} className="hover:text-fg-default transition-colors disabled:opacity-40 disabled:pointer-events-none">Move to folder…</button>
            <button type="button" disabled={!selected.size || busy} onClick={batchDownload} className="hover:text-fg-default transition-colors disabled:opacity-40 disabled:pointer-events-none">Download</button>
            <button type="button" disabled={!selected.size || busy} onClick={batchDelete} className="hover:text-fg-default transition-colors disabled:opacity-40 disabled:pointer-events-none">Delete</button>
            {busy && <span>working…</span>}
          </div>
        ) : undefined}
        layoutPlacement="header"
        layoutClassName="kol-helper-14"
        layoutOptions={layoutOptions}
        layout={layout}
        onLayoutChange={setLayout}
        /* BELOW `md` THE VIEWS AND THE SORT FOLD INTO ONE `···` (2026-10-08, apps/media/library at 390:
         * the FILES row, the three view icons and NAME · DATE · SIZE · KIND stacked three rows over the
         * list) — the browse page's fold, item 15, so the two pages fold the same way. Decided on the
         * recommendation, for review. */
        trailingActions={(
          <>
            <div className="hidden md:flex items-center gap-6"><ViewToggle viewMode={layout} onViewChange={setLayout} variant="icon" options={LAYOUT_OPTIONS} /></div>
            <div className="md:hidden">
              <MenuItem label={<Icon name="more" size={16} />} caret={false} align="end" size="sm" buttonClassName="shrink-0 px-2">
                {({ close }) => (
                  <div className="py-1 w-[200px]">
                    {LAYOUT_OPTIONS.map((opt) => (
                      <MenuDropdownItem key={opt.value} onClick={() => { setLayout(opt.value); close() }} shortcut={layout === opt.value ? <Icon name="check" size={11} /> : undefined}>{opt.label}</MenuDropdownItem>
                    ))}
                    <MenuDropdownDivider />
                    {SORT_OPTIONS.map((opt) => (
                      <MenuDropdownItem key={opt.value} onClick={() => { handleSort(opt.value); close() }} shortcut={sortBy === opt.value ? <Icon name={sortDir === 'desc' ? 'arrow-down' : 'arrow-up'} size={11} /> : undefined}>{opt.label}</MenuDropdownItem>
                    ))}
                  </div>
                )}
              </MenuItem>
            </div>
          </>
        )}
        belowActions={layout === 'off' ? null : <div className="h-8 hidden md:flex items-center"><SortControls options={SORT_OPTIONS} sortBy={sortBy} sortDir={sortDir} onSort={handleSort} /></div>}
        /* ONE WALL BODY, shared with the merged surface (2026-09-22) */
        renderItem={(filtered) => (
          <WallBody files={filtered} layout={layout} sortBy={sortBy} sortDir={sortDir} pageSize={pageSize} listId={listId}
            mediaUrl={mediaUrl} downloadUrl={downloadUrl} keySet={keySet} videoPreview={videoPreview} formatDate={formatDate}
            selected={selected} selectMode={selectMode} onToggleSelect={toggleSelect} onSorted={(list) => { sortedRef.current = list }}
            renderName={renderNameCell} renderActions={renderActions} />
        )}
      />

      {/* the pill again — the wall is one of the surfaces it switches, so it
          carries the same bar and owes it the same room (item 16) */}
      {tabs?.length > 0 && (
        <>
          <div className="md:hidden" aria-hidden="true" style={{ height: TABBAR_H }} />
          <TabsRow look="stacked" placement="phone" tabs={tabs} value={activeTab} onChange={onTabChange} />
        </>
      )}
    </div>
  )
}
