()
+ for (const post of posts) {
+ // Add original language (default: ja for Japanese posts)
+ const postLang = post.value.lang || 'ja'
+ availableLangs.add(postLang)
+ // Add translation languages
+ if (post.value.translations) {
+ for (const lang of Object.keys(post.value.translations)) {
+ availableLangs.add(lang)
+ }
+ }
+ }
+ const langList = Array.from(availableLangs)
+
+ // Build page
+ let html = renderHeader(handle)
+
+ // Mode tabs (Blog/Browser/Post/PDS)
+ const activeTab = route.type === 'postpage' ? 'post' :
+ (route.type === 'atbrowser' || route.type === 'service' || route.type === 'collection' || route.type === 'record' ? 'browser' : 'blog')
+ html += renderModeTabs(handle, activeTab)
+
+ // Profile section
+ if (profile) {
+ html += await renderProfile(did, profile, handle, webUrl)
+ }
+
+ // Check if logged-in user owns this content
+ const loggedInDid = getLoggedInDid()
+ const isOwner = isLoggedIn() && loggedInDid === did
+
+ // Content section based on route type
+ if (route.type === 'record' && route.collection && route.rkey) {
+ // AT-Browser: Single record view
+ const record = await getRecord(did, route.collection, route.rkey)
+ if (record) {
+ html += `${renderRecordDetail(record, route.collection, isOwner)}
`
+ } else {
+ html += `Record not found
`
+ }
+ html += ``
+
+ } else if (route.type === 'collection' && route.collection) {
+ // AT-Browser: Collection records list
+ const records = await listRecords(did, route.collection)
+ html += `${renderRecordList(records, handle, route.collection)}
`
+ const parts = route.collection.split('.')
+ const service = parts.length >= 2 ? `${parts[1]}.${parts[0]}` : ''
+ html += ``
+
+ } else if (route.type === 'service' && route.service) {
+ // AT-Browser: Service collections list
+ const collections = await describeRepo(did)
+ const filtered = filterCollectionsByService(collections, route.service)
+ html += `${renderCollectionList(filtered, handle, route.service)}
`
+ html += ``
+
+ } else if (route.type === 'atbrowser') {
+ // AT-Browser: Main view with server info + service list
+ const pds = await getPds(did)
+ const collections = await describeRepo(did)
+
+ html += ``
+ html += renderServerInfo(did, pds)
+ html += renderServiceList(collections, handle)
+ html += `
`
+ html += ``
+
+ } else if (route.type === 'post' && route.rkey) {
+ // Post detail (config.collection with markdown)
+ const post = await getPost(did, config.collection, route.rkey, localFirst)
+ html += renderLangSelector(langList)
+ if (post) {
+ html += `${renderPostDetail(post, handle, config.collection, isOwner, config.siteUrl, webUrl)}
`
+ } else {
+ html += `Post not found
`
+ }
+ html += ``
+
+ } else if (route.type === 'postpage') {
+ // Post form page
+ html += `${renderPostForm(config.collection)}
`
+ html += ``
+
+ } else {
+ // User page: compact collection buttons + posts
+ const collections = await describeRepo(did)
+ html += `${renderCollectionButtons(collections, handle)}
`
+
+ // Language selector above content
+ html += renderLangSelector(langList)
+
+ // Use pre-loaded posts
+ html += `${renderPostList(posts, handle)}
`
+ }
+
+ html += renderFooter(handle)
+
+ app.innerHTML = html
+ hideLoading(app)
+ setupEventHandlers()
+
+ // Setup mode tabs (PDS selector + Lang selector)
+ await setupModeTabs(
+ (_network) => {
+ // Refresh when network is changed
+ render(parseRoute())
+ },
+ langList,
+ (_lang) => {
+ // Refresh when language is changed
+ render(parseRoute())
+ }
+ )
+
+ // Setup post form on postpage
+ if (route.type === 'postpage' && isLoggedIn()) {
+ setupPostForm(config.collection, () => {
+ // Navigate to user page on success
+ navigate({ type: 'user', handle })
+ })
+ }
+
+ // Setup record delete button
+ if (isOwner) {
+ setupRecordDelete(handle, route)
+ setupPostEdit(config.collection)
+ }
+
+ // Setup post detail (translation toggle, discussion)
+ if (route.type === 'post') {
+ const contentEl = document.getElementById('content')
+ if (contentEl) {
+ setupPostDetail(contentEl)
+ }
+ }
+
+ } catch (error) {
+ console.error('Render error:', error)
+ app.innerHTML = `
+ ${renderHeader(currentHandle)}
+ Error: ${error}
+ ${renderFooter(currentHandle)}
+ `
+ hideLoading(app)
+ setupEventHandlers()
+ }
+}
+
+function setupEventHandlers(): void {
+ // Header form
+ const form = document.getElementById('header-form') as HTMLFormElement
+ const input = document.getElementById('header-input') as HTMLInputElement
+
+ form?.addEventListener('submit', (e) => {
+ e.preventDefault()
+ const handle = input.value.trim()
+ if (handle) {
+ navigate({ type: 'user', handle })
+ }
+ })
+
+ // Login button
+ const loginBtn = document.getElementById('login-btn')
+ loginBtn?.addEventListener('click', async () => {
+ const handle = input.value.trim() || currentHandle
+ if (handle) {
+ try {
+ await login(handle)
+ } catch (e) {
+ console.error('Login failed:', e)
+ alert('Login failed. Please check your handle.')
+ }
+ } else {
+ alert('Please enter a handle first.')
+ }
+ })
+
+ // Logout button
+ const logoutBtn = document.getElementById('logout-btn')
+ logoutBtn?.addEventListener('click', async () => {
+ await logout()
+ })
+}
+
+// Setup record delete button
+function setupRecordDelete(handle: string, _route: Route): void {
+ const deleteBtn = document.getElementById('record-delete-btn')
+ if (!deleteBtn) return
+
+ deleteBtn.addEventListener('click', async () => {
+ const collection = deleteBtn.getAttribute('data-collection')
+ const rkey = deleteBtn.getAttribute('data-rkey')
+
+ if (!collection || !rkey) return
+
+ if (!confirm('Are you sure you want to delete this record?')) return
+
+ try {
+ deleteBtn.textContent = 'Deleting...'
+ ;(deleteBtn as HTMLButtonElement).disabled = true
+
+ await deleteRecord(collection, rkey)
+
+ // Navigate back to collection list
+ navigate({ type: 'collection', handle, collection })
+ } catch (err) {
+ console.error('Delete failed:', err)
+ alert('Delete failed: ' + err)
+ deleteBtn.textContent = 'Delete'
+ ;(deleteBtn as HTMLButtonElement).disabled = false
+ }
+ })
+}
+
+// Setup post edit form
+function setupPostEdit(collection: string): void {
+ const editBtn = document.getElementById('post-edit-btn')
+ const editForm = document.getElementById('post-edit-form')
+ const postDisplay = document.getElementById('post-display')
+ const cancelBtn = document.getElementById('post-edit-cancel')
+ const saveBtn = document.getElementById('post-edit-save')
+ const titleInput = document.getElementById('post-edit-title') as HTMLInputElement
+ const contentInput = document.getElementById('post-edit-content') as HTMLTextAreaElement
+
+ if (!editBtn || !editForm) return
+
+ // Show edit form
+ editBtn.addEventListener('click', () => {
+ if (postDisplay) postDisplay.style.display = 'none'
+ editForm.style.display = 'block'
+ editBtn.style.display = 'none'
+ })
+
+ // Cancel edit
+ cancelBtn?.addEventListener('click', () => {
+ editForm.style.display = 'none'
+ if (postDisplay) postDisplay.style.display = ''
+ editBtn.style.display = ''
+ })
+
+ // Save edit
+ saveBtn?.addEventListener('click', async () => {
+ const rkey = saveBtn.getAttribute('data-rkey')
+ if (!rkey || !titleInput || !contentInput) return
+
+ const title = titleInput.value.trim()
+ const content = contentInput.value.trim()
+
+ if (!title || !content) {
+ alert('Title and content are required')
+ return
+ }
+
+ try {
+ saveBtn.textContent = 'Saving...'
+ ;(saveBtn as HTMLButtonElement).disabled = true
+
+ await updatePost(collection, rkey, title, content)
+
+ // Refresh the page
+ render(parseRoute())
+ } catch (err) {
+ console.error('Update failed:', err)
+ alert('Update failed: ' + err)
+ saveBtn.textContent = 'Save'
+ ;(saveBtn as HTMLButtonElement).disabled = false
+ }
+ })
+}
+
+// Initial render
+render(parseRoute())
+
+// Handle route changes
+onRouteChange(render)
diff --git a/src/web/styles/main.css b/src/web/styles/main.css
new file mode 100644
index 0000000..f139446
--- /dev/null
+++ b/src/web/styles/main.css
@@ -0,0 +1,2190 @@
+:root {
+ --btn-color: #0066cc;
+}
+
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
+ line-height: 1.6;
+ color: #1a1a1a;
+ background: #fff;
+}
+
+#app {
+ max-width: 800px;
+ margin: 0 auto;
+ padding: 20px;
+ position: relative;
+}
+
+/* Loading */
+.loading-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(255, 255, 255, 0.8);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+}
+
+.loading-spinner {
+ width: 40px;
+ height: 40px;
+ border: 3px solid #f0f0f0;
+ border-top-color: var(--btn-color);
+ border-radius: 50%;
+ animation: spin 0.8s linear infinite;
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+.loading-small {
+ padding: 12px;
+ text-align: center;
+ color: #666;
+ font-size: 14px;
+}
+
+/* Dark mode */
+@media (prefers-color-scheme: dark) {
+ body {
+ background: #0a0a0a;
+ color: #e0e0e0;
+ }
+ .tab {
+ color: #888;
+ }
+ .tab:hover {
+ background: #2a2a2a;
+ }
+ .tab.active {
+ background: var(--btn-color);
+ color: #fff;
+ }
+ .pds-dropdown {
+ background: #1a1a1a;
+ border-color: #333;
+ }
+ .pds-option {
+ color: #e0e0e0;
+ }
+ .pds-option:hover {
+ background: #2a2a2a;
+ }
+ .pds-option.selected {
+ background: linear-gradient(135deg, #1a2a3a 0%, #152535 100%);
+ }
+ .pds-name {
+ color: #e0e0e0;
+ }
+ .pds-check {
+ border-color: #555;
+ }
+ .profile {
+ background: #16181c;
+ border-color: #2f3336;
+ }
+ .profile-name {
+ color: #e7e9ea;
+ }
+ .profile-handle,
+ .profile-handle-link {
+ color: #71767b;
+ }
+ .profile-description {
+ color: #e7e9ea;
+ }
+ .profile-avatar-placeholder {
+ background: #2f3336;
+ }
+ .service-item {
+ background: #2a2a2a;
+ color: #e0e0e0;
+ }
+ .service-item:hover {
+ background: #333;
+ }
+ .post-item {
+ border-color: #333;
+ }
+ .post-link:hover {
+ background: #1a1a1a;
+ }
+ .browser-input {
+ background: #1a1a1a;
+ border-color: #333;
+ color: #e0e0e0;
+ }
+}
+
+/* Header */
+#header {
+ margin-bottom: 24px;
+}
+
+.header-form {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+}
+
+.header-input {
+ flex: 1;
+ padding: 8px 12px;
+ border: 1px solid #ddd;
+ border-radius: 6px;
+ font-size: 14px;
+}
+
+.header-btn {
+ width: 36px;
+ height: 36px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: #f0f0f0;
+ color: #333;
+ border: 1px solid #ddd;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 16px;
+ font-weight: bold;
+}
+
+.header-btn:hover {
+ background: #e0e0e0;
+}
+
+.header-btn.at-btn {
+ background: var(--btn-color);
+ color: #fff;
+ border-color: var(--btn-color);
+}
+
+.header-btn.at-btn:hover {
+ background: var(--btn-color);
+ filter: brightness(0.85);
+}
+
+.header-btn.login-btn {
+ color: #666;
+}
+
+.login-icon {
+ width: 18px;
+ height: 18px;
+ opacity: 0.6;
+}
+
+.header-btn.login-btn:hover .login-icon {
+ opacity: 0.9;
+}
+
+.header-btn.user-btn {
+ width: auto;
+ padding: 8px 12px;
+ background: var(--btn-color);
+ color: #fff;
+ border-color: var(--btn-color);
+ font-size: 13px;
+ font-weight: 500;
+}
+
+/* Post Form */
+.post-form-container {
+ padding: 20px 0;
+}
+
+.post-form-container h3 {
+ font-size: 18px;
+ margin-bottom: 16px;
+}
+
+.post-form {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.post-form-title {
+ padding: 10px 12px;
+ border: 1px solid #ddd;
+ border-radius: 6px;
+ font-size: 16px;
+}
+
+.post-form-body {
+ padding: 10px 12px;
+ border: 1px solid #ddd;
+ border-radius: 6px;
+ font-size: 14px;
+ resize: vertical;
+ min-height: 120px;
+ font-family: inherit;
+}
+
+.post-form-footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.post-form-collection {
+ font-size: 12px;
+ color: #888;
+ font-family: monospace;
+}
+
+.post-form-btn {
+ padding: 10px 24px;
+ background: var(--btn-color);
+ color: #fff;
+ border: none;
+ border-radius: 6px;
+ font-size: 14px;
+ cursor: pointer;
+}
+
+.post-form-btn:hover {
+ background: #0052a3;
+}
+
+.post-form-btn:disabled {
+ background: #ccc;
+ cursor: not-allowed;
+}
+
+.post-status {
+ margin-top: 12px;
+}
+
+.post-success {
+ color: #155724;
+}
+
+.post-error {
+ color: #dc3545;
+}
+
+/* Mode Tabs */
+.mode-tabs {
+ display: flex;
+ gap: 4px;
+ margin-bottom: 16px;
+}
+
+.tab {
+ padding: 8px 16px;
+ text-decoration: none;
+ color: #666;
+ border: none;
+ background: none;
+ border-radius: 6px;
+ font-size: 14px;
+ cursor: pointer;
+}
+
+.tab:hover {
+ background: #f0f0f0;
+}
+
+.tab.active {
+ background: var(--btn-color);
+ color: #fff;
+}
+
+/* PDS Selector */
+.pds-selector {
+ position: relative;
+ margin-left: auto;
+}
+
+.pds-dropdown {
+ display: none;
+ position: absolute;
+ top: 100%;
+ right: 0;
+ margin-top: 4px;
+ background: #fff;
+ border: 1px solid #ddd;
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ min-width: 180px;
+ z-index: 100;
+ overflow: hidden;
+}
+
+.pds-dropdown.show {
+ display: block;
+}
+
+.pds-option {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px 16px;
+ cursor: pointer;
+ font-size: 14px;
+ transition: background 0.15s;
+}
+
+.pds-option:hover {
+ background: #f5f5f5;
+}
+
+.pds-option.selected {
+ background: linear-gradient(135deg, #f0f7ff 0%, #e8f4ff 100%);
+}
+
+.pds-name {
+ color: #333;
+ font-weight: 500;
+}
+
+.pds-check {
+ width: 18px;
+ height: 18px;
+ border-radius: 50%;
+ border: 2px solid #ccc;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 10px;
+ transition: all 0.2s;
+ color: transparent;
+}
+
+.pds-option.selected .pds-check {
+ background: var(--btn-color);
+ border-color: var(--btn-color);
+ color: #fff;
+}
+
+/* Profile */
+.profile {
+ display: flex;
+ gap: 16px;
+ padding: 20px;
+ background: #f5f5f5;
+ border-radius: 12px;
+ margin-bottom: 24px;
+}
+
+.profile-avatar {
+ width: 80px;
+ height: 80px;
+ border-radius: 50%;
+ object-fit: cover;
+}
+
+.profile-avatar-placeholder {
+ width: 80px;
+ height: 80px;
+ border-radius: 50%;
+ background: #e0e0e0;
+}
+
+.profile-info {
+ flex: 1;
+}
+
+.profile-name {
+ font-size: 20px;
+ font-weight: 600;
+ margin-bottom: 4px;
+}
+
+.profile-handle {
+ font-size: 14px;
+ color: #666;
+ margin-bottom: 8px;
+}
+
+.profile-handle-link {
+ color: #666;
+ text-decoration: none;
+}
+
+.profile-handle-link:hover {
+ color: var(--btn-color);
+ text-decoration: underline;
+}
+
+.profile-desc {
+ font-size: 14px;
+ color: #444;
+}
+
+/* Services */
+.services {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-top: 12px;
+}
+
+.service-item {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 6px 12px;
+ background: #f5f5f5;
+ border-radius: 20px;
+ text-decoration: none;
+ color: #333;
+ font-size: 13px;
+ transition: background 0.2s;
+}
+
+.service-item:hover {
+ background: #e8e8e8;
+}
+
+.service-favicon {
+ width: 16px;
+ height: 16px;
+}
+
+.service-name {
+ font-weight: 500;
+}
+
+/* Post List */
+.post-list {
+ list-style: none;
+ margin-top: 24px;
+ margin-bottom: 24px;
+}
+
+.post-item {
+ border-bottom: 1px solid #eee;
+}
+
+.post-link {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 16px 8px;
+ text-decoration: none;
+ color: inherit;
+}
+
+.post-link:hover {
+ background: #f9f9f9;
+}
+
+.post-title {
+ font-weight: 500;
+}
+
+.post-date {
+ font-size: 13px;
+ color: #888;
+}
+
+/* New post from API (not in static) */
+.post-item-new {
+ animation: fadeIn 0.3s ease-in;
+}
+
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(-10px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+/* Post Form */
+.post-form-container {
+ padding: 20px 0;
+ margin-bottom: 32px;
+}
+
+.post-form {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.post-form-title {
+ padding: 10px 12px;
+ border: 1px solid #ddd;
+ border-radius: 6px;
+ font-size: 16px;
+}
+
+.post-form-body {
+ padding: 10px 12px;
+ border: 1px solid #ddd;
+ border-radius: 6px;
+ font-size: 14px;
+ resize: vertical;
+ min-height: 120px;
+ font-family: inherit;
+}
+
+.post-form-footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.post-form-collection {
+ font-size: 12px;
+ color: #888;
+ font-family: monospace;
+}
+
+.post-form-btn {
+ padding: 10px 24px;
+ background: var(--btn-color);
+ color: #fff;
+ border: none;
+ border-radius: 6px;
+ font-size: 14px;
+ cursor: pointer;
+}
+
+.post-form-btn:hover {
+ opacity: 0.9;
+}
+
+.post-form-btn:disabled {
+ background: #ccc;
+ cursor: not-allowed;
+}
+
+.post-status {
+ margin-top: 12px;
+}
+
+.post-success {
+ color: #155724;
+}
+
+.post-error {
+ color: #dc3545;
+}
+
+/* Post Detail */
+.post-detail {
+ padding: 20px 0;
+}
+
+.post-header {
+ margin-bottom: 24px;
+ padding-bottom: 16px;
+ border-bottom: 1px solid #eee;
+}
+
+.post-header .post-title {
+ font-size: 28px;
+ font-weight: 600;
+ margin-bottom: 8px;
+}
+
+.post-meta {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.post-header .post-date {
+ font-size: 14px;
+ color: #888;
+}
+
+.json-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 4px 8px;
+ background: #f0f0f0;
+ color: #666;
+ border-radius: 4px;
+ text-decoration: none;
+ font-family: monospace;
+ font-size: 12px;
+}
+
+.json-btn:hover {
+ background: #e0e0e0;
+ color: #333;
+}
+
+/* Record Delete Button */
+.record-delete-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 6px 12px;
+ margin-top: 12px;
+ background: #dc3545;
+ color: #fff;
+ border: none;
+ border-radius: 4px;
+ font-size: 13px;
+ cursor: pointer;
+ transition: background 0.2s;
+}
+
+.record-delete-btn:hover {
+ background: #c82333;
+}
+
+.record-delete-btn:disabled {
+ background: #6c757d;
+ cursor: not-allowed;
+}
+
+/* Post Edit Button */
+.post-edit-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 4px 10px;
+ margin-left: 8px;
+ background: var(--btn-color);
+ color: #fff;
+ border: none;
+ border-radius: 4px;
+ font-size: 12px;
+ cursor: pointer;
+ transition: opacity 0.2s;
+}
+
+.post-edit-btn:hover {
+ opacity: 0.85;
+}
+
+/* Post Edit Form */
+.post-edit-form {
+ margin-top: 16px;
+ padding: 16px;
+ background: #f9f9f9;
+ border-radius: 8px;
+}
+
+.post-edit-title {
+ width: 100%;
+ padding: 10px 12px;
+ margin-bottom: 12px;
+ border: 1px solid #ddd;
+ border-radius: 6px;
+ font-size: 16px;
+ font-weight: 600;
+}
+
+.post-edit-content {
+ width: 100%;
+ padding: 12px;
+ border: 1px solid #ddd;
+ border-radius: 6px;
+ font-size: 14px;
+ font-family: inherit;
+ resize: vertical;
+ min-height: 200px;
+}
+
+.post-edit-actions {
+ display: flex;
+ gap: 8px;
+ margin-top: 12px;
+ justify-content: flex-end;
+}
+
+.post-edit-cancel {
+ padding: 8px 16px;
+ background: #6c757d;
+ color: #fff;
+ border: none;
+ border-radius: 6px;
+ font-size: 14px;
+ cursor: pointer;
+}
+
+.post-edit-cancel:hover {
+ background: #5a6268;
+}
+
+.post-edit-save {
+ padding: 8px 16px;
+ background: var(--btn-color);
+ color: #fff;
+ border: none;
+ border-radius: 6px;
+ font-size: 14px;
+ cursor: pointer;
+}
+
+.post-edit-save:hover {
+ opacity: 0.9;
+}
+
+.post-edit-save:disabled {
+ background: #6c757d;
+ cursor: not-allowed;
+}
+
+.edit-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 4px 8px;
+ background: #28a745;
+ color: #fff;
+ border: none;
+ border-radius: 4px;
+ text-decoration: none;
+ font-family: monospace;
+ font-size: 12px;
+ cursor: pointer;
+}
+
+.edit-btn:hover {
+ background: #218838;
+}
+
+/* Discussion */
+.discussion-section {
+ margin-top: 32px;
+ padding-top: 24px;
+ border-top: 1px solid #e0e0e0;
+}
+
+.discussion-link {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 16px;
+ background: #f5f5f5;
+ color: #333;
+ text-decoration: none;
+ border-radius: 8px;
+ font-size: 14px;
+ font-weight: 500;
+ transition: background 0.2s;
+}
+
+.discussion-link:hover {
+ background: #e8e8e8;
+}
+
+.discussion-link svg {
+ color: var(--btn-color);
+}
+
+.discussion-posts {
+ margin-top: 16px;
+}
+
+.discussion-post {
+ display: block;
+ padding: 12px;
+ margin-bottom: 8px;
+ background: #fafafa;
+ border-radius: 8px;
+ text-decoration: none;
+ color: inherit;
+ transition: background 0.2s;
+}
+
+.discussion-post:hover {
+ background: #f0f0f0;
+}
+
+.discussion-author {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 8px;
+}
+
+.discussion-avatar {
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ object-fit: cover;
+}
+
+.discussion-avatar-placeholder {
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ background: #ddd;
+}
+
+.discussion-author-info {
+ flex: 1;
+ min-width: 0;
+}
+
+.discussion-name {
+ display: block;
+ font-size: 14px;
+ font-weight: 600;
+ color: #333;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.discussion-handle {
+ display: block;
+ font-size: 12px;
+ color: #666;
+}
+
+.discussion-date {
+ font-size: 12px;
+ color: #888;
+ white-space: nowrap;
+}
+
+.discussion-text {
+ font-size: 14px;
+ color: #444;
+ line-height: 1.5;
+ word-break: break-word;
+}
+
+
+/* Edit Form */
+.edit-form-container {
+ padding: 20px 0;
+}
+
+.edit-form-container h3 {
+ font-size: 18px;
+ margin-bottom: 16px;
+}
+
+.edit-form {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.edit-form-title {
+ padding: 10px 12px;
+ border: 1px solid #ddd;
+ border-radius: 6px;
+ font-size: 16px;
+}
+
+.edit-form-body {
+ padding: 10px 12px;
+ border: 1px solid #ddd;
+ border-radius: 6px;
+ font-size: 14px;
+ resize: vertical;
+ min-height: 200px;
+ font-family: inherit;
+}
+
+.edit-form-footer {
+ display: flex;
+ justify-content: flex-end;
+ gap: 8px;
+}
+
+.edit-cancel-btn {
+ padding: 10px 24px;
+ background: #6c757d;
+ color: #fff;
+ border: none;
+ border-radius: 6px;
+ font-size: 14px;
+ cursor: pointer;
+}
+
+.edit-cancel-btn:hover {
+ background: #5a6268;
+}
+
+.edit-submit-btn {
+ padding: 10px 24px;
+ background: #28a745;
+ color: #fff;
+ border: none;
+ border-radius: 6px;
+ font-size: 14px;
+ cursor: pointer;
+}
+
+.edit-submit-btn:hover {
+ background: #218838;
+}
+
+.edit-submit-btn:disabled {
+ background: #ccc;
+ cursor: not-allowed;
+}
+
+/* Discussion Section */
+.discussion-section {
+ margin-top: 48px;
+ padding-top: 24px;
+ border-top: 1px solid #eee;
+}
+
+.discussion-section h3 {
+ font-size: 18px;
+ margin-bottom: 16px;
+}
+
+.discuss-link {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 16px;
+ background: var(--btn-color);
+ color: #fff;
+ border-radius: 20px;
+ text-decoration: none;
+ font-size: 14px;
+ font-weight: 500;
+}
+
+.discuss-link:hover {
+ background: var(--btn-color);
+ filter: brightness(0.85);
+}
+
+.discuss-link svg {
+ width: 18px;
+ height: 18px;
+}
+
+.discussion-posts {
+ margin-top: 20px;
+}
+
+.loading-small {
+ color: #888;
+ font-size: 14px;
+}
+
+.no-discussion {
+ color: #888;
+ font-size: 14px;
+}
+
+.discussion-post {
+ display: block;
+ padding: 16px;
+ margin-bottom: 12px;
+ background: #f9f9f9;
+ border-radius: 8px;
+ text-decoration: none;
+ color: inherit;
+}
+
+.discussion-post:hover {
+ background: #f0f0f0;
+}
+
+.discussion-author {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-bottom: 8px;
+}
+
+.discussion-avatar {
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+}
+
+.discussion-author-info {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+}
+
+.discussion-name {
+ font-weight: 600;
+ font-size: 14px;
+}
+
+.discussion-handle {
+ font-size: 12px;
+ color: #888;
+}
+
+.discussion-date {
+ font-size: 12px;
+ color: #888;
+}
+
+.discussion-text {
+ font-size: 14px;
+ line-height: 1.5;
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+
+.post-content {
+ font-size: 16px;
+ line-height: 1.8;
+}
+
+/* Markdown Styles */
+.post-content h1,
+.post-content h2,
+.post-content h3,
+.post-content h4,
+.post-content h5,
+.post-content h6 {
+ margin-top: 1.5em;
+ margin-bottom: 0.5em;
+ font-weight: 600;
+ line-height: 1.3;
+}
+
+.post-content h1 { font-size: 1.75em; }
+.post-content h2 { font-size: 1.5em; }
+.post-content h3 { font-size: 1.25em; }
+.post-content h4 { font-size: 1.1em; }
+
+.post-content p {
+ margin-bottom: 1em;
+}
+
+.post-content ul,
+.post-content ol {
+ margin-bottom: 1em;
+ padding-left: 1.5em;
+}
+
+.post-content li {
+ margin-bottom: 0.25em;
+}
+
+.post-content a {
+ color: var(--btn-color);
+ text-decoration: none;
+}
+
+.post-content a:hover {
+ text-decoration: underline;
+}
+
+.post-content blockquote {
+ margin: 1em 0;
+ padding: 0.5em 1em;
+ border-left: 4px solid #ddd;
+ background: #f9f9f9;
+ color: #666;
+}
+
+.post-content code {
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
+ font-size: 0.9em;
+ padding: 0.15em 0.4em;
+ background: #f0f0f0;
+ border-radius: 4px;
+}
+
+.post-content pre {
+ margin: 1em 0;
+ padding: 1em;
+ background: #1e1e1e;
+ border-radius: 8px;
+ overflow-x: auto;
+}
+
+.post-content pre code {
+ display: block;
+ padding: 0;
+ background: transparent;
+ color: #d4d4d4;
+ font-size: 14px;
+ line-height: 1.5;
+}
+
+.post-content img {
+ max-width: 100%;
+ height: auto;
+ border-radius: 8px;
+}
+
+.post-content hr {
+ margin: 2em 0;
+ border: none;
+ border-top: 1px solid #eee;
+}
+
+.post-content table {
+ width: 100%;
+ margin: 1em 0;
+ border-collapse: collapse;
+}
+
+.post-content th,
+.post-content td {
+ padding: 0.5em;
+ border: 1px solid #ddd;
+ text-align: left;
+}
+
+.post-content th {
+ background: #f5f5f5;
+ font-weight: 600;
+}
+
+/* Highlight.js Theme Overrides */
+.hljs-keyword,
+.hljs-selector-tag,
+.hljs-built_in,
+.hljs-name,
+.hljs-tag { color: #569cd6; }
+.hljs-string,
+.hljs-title,
+.hljs-section,
+.hljs-attribute,
+.hljs-literal,
+.hljs-template-tag,
+.hljs-template-variable,
+.hljs-type,
+.hljs-addition { color: #ce9178; }
+.hljs-comment,
+.hljs-quote,
+.hljs-deletion,
+.hljs-meta { color: #6a9955; }
+.hljs-number,
+.hljs-regexp,
+.hljs-symbol,
+.hljs-variable,
+.hljs-link { color: #b5cea8; }
+.hljs-function { color: #dcdcaa; }
+.hljs-attr { color: #9cdcfe; }
+
+.post-footer {
+ margin-top: 32px;
+ padding-top: 16px;
+ border-top: 1px solid #eee;
+}
+
+.back-link {
+ color: var(--btn-color);
+ text-decoration: none;
+}
+
+.back-link:hover {
+ text-decoration: underline;
+}
+
+/* Utility */
+.no-posts,
+.no-data,
+.error {
+ padding: 40px;
+ text-align: center;
+ color: #888;
+}
+
+.loading {
+ padding: 40px;
+ text-align: center;
+ color: #666;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 12px;
+}
+
+.loading-spinner {
+ width: 24px;
+ height: 24px;
+ border: 3px solid #e0e0e0;
+ border-top-color: var(--btn-color);
+ border-radius: 50%;
+ animation: spin 0.8s linear infinite;
+}
+
+@keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+/* Skeleton UI */
+.browser-skeleton {
+ padding: 16px 0;
+}
+
+.skeleton-header {
+ margin-bottom: 16px;
+}
+
+.skeleton-title {
+ width: 120px;
+ height: 20px;
+ background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
+ background-size: 200% 100%;
+ animation: shimmer 1.5s infinite;
+ border-radius: 4px;
+}
+
+.skeleton-list {
+ list-style: none;
+}
+
+.skeleton-item {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 12px 8px;
+ border-bottom: 1px solid #eee;
+}
+
+.skeleton-icon {
+ width: 24px;
+ height: 24px;
+ background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
+ background-size: 200% 100%;
+ animation: shimmer 1.5s infinite;
+ border-radius: 4px;
+}
+
+.skeleton-text {
+ flex: 1;
+ height: 16px;
+ background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
+ background-size: 200% 100%;
+ animation: shimmer 1.5s infinite;
+ border-radius: 4px;
+ max-width: 200px;
+}
+
+@keyframes shimmer {
+ 0% {
+ background-position: -200% 0;
+ }
+ 100% {
+ background-position: 200% 0;
+ }
+}
+
+@media (prefers-color-scheme: dark) {
+ .skeleton-title,
+ .skeleton-icon,
+ .skeleton-text {
+ background: linear-gradient(90deg, #2a2a2a 25%, #333 50%, #2a2a2a 75%);
+ background-size: 200% 100%;
+ animation: shimmer 1.5s infinite;
+ }
+ .skeleton-item {
+ border-color: #333;
+ }
+}
+
+/* Footer Links */
+.footer-links {
+ display: flex;
+ justify-content: center;
+ gap: 20px;
+ margin-top: 40px;
+ padding: 20px 0;
+}
+
+.footer-link-item {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 40px;
+ height: 40px;
+ color: #666;
+ text-decoration: none;
+ transition: color 0.2s;
+}
+
+.footer-link-item:hover {
+ color: var(--btn-color);
+}
+
+.footer-link-item svg {
+ width: 24px;
+ height: 24px;
+}
+
+.footer-link-item [class^="icon-"] {
+ font-size: 24px;
+}
+
+.footer-link-favicon {
+ width: 24px;
+ height: 24px;
+}
+
+@media (prefers-color-scheme: dark) {
+ .footer-link-item {
+ color: #888;
+ }
+ .footer-link-item:hover {
+ color: var(--btn-color);
+ }
+}
+
+/* Footer */
+.site-footer {
+ margin-top: 20px;
+ padding: 20px 0;
+ text-align: center;
+ font-size: 13px;
+ color: #888;
+}
+
+.site-footer p {
+ margin: 4px 0;
+}
+
+/* Language Selector (above content) */
+.lang-selector {
+ display: flex;
+ justify-content: flex-end;
+ margin-bottom: 8px;
+ position: relative;
+}
+
+.lang-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: transparent;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ color: #666;
+ padding: 8px 16px;
+}
+
+.lang-btn:hover {
+ background: #f0f0f0;
+}
+
+.lang-icon {
+ width: 20px;
+ height: 20px;
+ opacity: 0.6;
+}
+
+.lang-btn:hover .lang-icon {
+ opacity: 0.9;
+}
+
+.lang-dropdown {
+ display: none;
+ position: absolute;
+ top: 100%;
+ right: 0;
+ margin-top: 4px;
+ background: #fff;
+ border: 1px solid #ddd;
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ min-width: 100px;
+ z-index: 100;
+ overflow: hidden;
+}
+
+.lang-dropdown.show {
+ display: block;
+}
+
+.lang-option {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px 16px;
+ cursor: pointer;
+ font-size: 14px;
+ transition: background 0.15s;
+}
+
+.lang-option:hover {
+ background: #f5f5f5;
+}
+
+.lang-option.selected {
+ background: linear-gradient(135deg, #f0f7ff 0%, #e8f4ff 100%);
+}
+
+.lang-name {
+ color: #333;
+ font-weight: 500;
+}
+
+.lang-check {
+ width: 18px;
+ height: 18px;
+ border-radius: 50%;
+ border: 2px solid #ccc;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 10px;
+ transition: all 0.2s;
+}
+
+.lang-option.selected .lang-check {
+ background: var(--btn-color);
+ border-color: var(--btn-color);
+ color: #fff;
+}
+
+.lang-option:not(.selected) .lang-check {
+ color: transparent;
+}
+
+/* Content Header (above post list) */
+.content-header {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ margin-bottom: 8px;
+}
+
+@media (prefers-color-scheme: dark) {
+ .lang-btn {
+ background: transparent;
+ color: #888;
+ }
+ .lang-btn:hover {
+ background: #2a2a2a;
+ }
+ .lang-icon {
+ filter: invert(0.7);
+ }
+ .lang-dropdown {
+ background: #1a1a1a;
+ border-color: #333;
+ }
+ .lang-option:hover {
+ background: #2a2a2a;
+ }
+ .lang-option.selected {
+ background: linear-gradient(135deg, #1a2a3a 0%, #1a3040 100%);
+ }
+ .lang-name {
+ color: #e0e0e0;
+ }
+}
+
+/* AT Browser */
+.server-info {
+ padding: 16px 0;
+ border-bottom: 1px solid #eee;
+ margin-bottom: 8px;
+}
+
+.server-info h3 {
+ font-size: 18px;
+ margin-bottom: 12px;
+}
+
+.server-details {
+ font-size: 13px;
+}
+
+.server-row {
+ display: flex;
+ gap: 12px;
+ padding: 6px 0;
+}
+
+.server-row dt {
+ font-weight: 600;
+ min-width: 40px;
+ color: #666;
+}
+
+.server-row dd {
+ font-family: 'SF Mono', Monaco, monospace;
+ font-size: 12px;
+ word-break: break-all;
+ color: #333;
+}
+
+.services-list,
+.collections,
+.records,
+.record-detail {
+ padding: 16px 0;
+}
+
+.services-list h3,
+.collections h3,
+.records h3,
+.record-detail h3 {
+ font-size: 18px;
+ margin-bottom: 12px;
+}
+
+.service-list {
+ list-style: none;
+}
+
+.service-list-item {
+ border-bottom: 1px solid #eee;
+}
+
+.service-list-link {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 12px 8px;
+ text-decoration: none;
+ color: inherit;
+}
+
+.service-list-link:hover {
+ background: #f9f9f9;
+}
+
+.service-list-favicon {
+ width: 24px;
+ height: 24px;
+}
+
+.service-list-name {
+ flex: 1;
+ font-weight: 500;
+}
+
+.service-list-count {
+ font-size: 13px;
+ color: #888;
+}
+
+.collection-header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.collection-header-favicon {
+ width: 24px;
+ height: 24px;
+}
+
+.collection-list,
+.record-list {
+ list-style: none;
+}
+
+.collection-item,
+.record-item {
+ border-bottom: 1px solid #eee;
+}
+
+.collection-link,
+.record-link {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 12px 8px;
+ text-decoration: none;
+ color: inherit;
+ font-family: monospace;
+ font-size: 14px;
+}
+
+.collection-link:hover,
+.record-link:hover {
+ background: #f9f9f9;
+}
+
+.collection-nsid {
+ flex: 1;
+}
+
+.record-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.record-item .record-link {
+ flex: 1;
+}
+
+.delete-btn-small {
+ width: 28px;
+ height: 28px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: transparent;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ color: #999;
+ cursor: pointer;
+ font-size: 16px;
+ flex-shrink: 0;
+ margin-right: 8px;
+}
+
+.delete-btn-small:hover {
+ background: #fee;
+ border-color: #f88;
+ color: #c00;
+}
+
+.collection-link,
+.record-link {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 12px 8px;
+ text-decoration: none;
+ color: inherit;
+ font-family: monospace;
+ font-size: 14px;
+}
+
+.collection-link:hover,
+.record-link:hover {
+ background: #f9f9f9;
+}
+
+.collection-favicon {
+ width: 20px;
+ height: 20px;
+ flex-shrink: 0;
+}
+
+.collection-nsid {
+ flex: 1;
+}
+
+.collection-service {
+ font-size: 12px;
+ color: #888;
+ font-family: -apple-system, BlinkMacSystemFont, sans-serif;
+}
+
+.record-link {
+ display: flex;
+ gap: 16px;
+ overflow: hidden;
+ min-width: 0;
+}
+
+.record-rkey {
+ color: var(--btn-color);
+ min-width: 120px;
+ flex-shrink: 0;
+}
+
+.record-preview {
+ color: #666;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ flex: 1;
+ min-width: 0;
+}
+
+.record-count {
+ font-size: 13px;
+ color: #888;
+ margin-bottom: 12px;
+}
+
+/* Record Detail */
+.record-header {
+ margin-bottom: 16px;
+ padding-bottom: 16px;
+ border-bottom: 1px solid #eee;
+}
+
+.record-uri,
+.record-cid {
+ font-family: monospace;
+ font-size: 12px;
+ color: #666;
+ margin: 4px 0;
+ word-break: break-all;
+}
+
+.schema-status {
+ display: inline-block;
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 12px;
+ margin-top: 8px;
+}
+
+.schema-verified {
+ background: #d4edda;
+ color: #155724;
+}
+
+.schema-none {
+ background: #f0f0f0;
+ color: #666;
+}
+
+.delete-btn {
+ display: inline-block;
+ padding: 6px 12px;
+ background: #dc3545;
+ color: #fff;
+ border: none;
+ border-radius: 4px;
+ font-size: 12px;
+ cursor: pointer;
+ margin-left: 8px;
+}
+
+.delete-btn:hover {
+ background: #c82333;
+}
+
+.delete-btn:disabled {
+ background: #999;
+ cursor: not-allowed;
+}
+
+/* JSON View */
+.json-view {
+ background: #f5f5f5;
+ border-radius: 8px;
+ padding: 16px;
+ overflow-x: auto;
+}
+
+.json-view pre {
+ margin: 0;
+}
+
+.json-view code {
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
+ font-size: 13px;
+ line-height: 1.5;
+}
+
+/* Dark mode additions */
+@media (prefers-color-scheme: dark) {
+ .header-input {
+ background: #1a1a1a;
+ border-color: #333;
+ color: #e0e0e0;
+ }
+ .header-btn {
+ background: #2a2a2a;
+ border-color: #333;
+ color: #e0e0e0;
+ }
+ .header-btn:hover {
+ background: #333;
+ }
+ .header-btn.at-btn,
+ .header-btn.user-btn {
+ background: var(--btn-color);
+ border-color: var(--btn-color);
+ color: #fff;
+ }
+ .post-form-title,
+ .post-form-body {
+ background: #1a1a1a;
+ border-color: #333;
+ color: #e0e0e0;
+ }
+ .json-btn {
+ background: #2a2a2a;
+ color: #888;
+ }
+ .json-btn:hover {
+ background: #333;
+ color: #e0e0e0;
+ }
+ .edit-form-title,
+ .edit-form-body {
+ background: #1a1a1a;
+ border-color: #333;
+ color: #e0e0e0;
+ }
+ .tab:hover {
+ background: #333;
+ }
+ .tab.active {
+ background: var(--btn-color);
+ }
+ .service-list-link:hover,
+ .collection-link:hover,
+ .record-link:hover {
+ background: #1a1a1a;
+ }
+ .service-list-item,
+ .collection-item,
+ .record-item,
+ .record-header {
+ border-color: #333;
+ }
+ .json-view {
+ background: #1a1a1a;
+ }
+ .schema-verified {
+ background: #1e3a29;
+ color: #75b798;
+ }
+ .schema-none {
+ background: #2a2a2a;
+ color: #888;
+ }
+ .delete-btn {
+ background: #dc3545;
+ }
+ .delete-btn:hover {
+ background: #c82333;
+ }
+ /* Dark mode markdown */
+ .post-content blockquote {
+ border-color: #444;
+ background: #1a1a1a;
+ color: #aaa;
+ }
+ .post-content code {
+ background: #2a2a2a;
+ }
+ .post-content th {
+ background: #2a2a2a;
+ }
+ .post-content th,
+ .post-content td {
+ border-color: #444;
+ }
+ .post-content hr {
+ border-color: #333;
+ }
+}
+
+/* Browser Section */
+#browser {
+ margin: 32px 0;
+}
+
+#content {
+ margin: 48px 0;
+}
+
+.browser-section {
+ margin-bottom: 20px;
+}
+
+.browser-title {
+ font-size: 14px;
+ font-weight: 600;
+ color: #666;
+ margin-bottom: 12px;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+/* Collection buttons (horizontal) */
+.collection-buttons {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-bottom: 20px;
+}
+
+.collection-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 6px 12px;
+ background: #f5f5f5;
+ border-radius: 20px;
+ text-decoration: none;
+ color: #333;
+ font-size: 13px;
+ transition: background 0.2s;
+}
+
+.collection-btn:hover {
+ background: #e8e8e8;
+}
+
+.collection-btn-icon {
+ width: 16px;
+ height: 16px;
+}
+
+.no-collections,
+.no-records {
+ padding: 20px;
+ text-align: center;
+ color: #888;
+ font-size: 14px;
+}
+
+/* Record list in browser */
+.record-list {
+ list-style: none;
+}
+
+.record-item {
+ border-bottom: 1px solid #eee;
+}
+
+.record-link {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px 8px;
+ text-decoration: none;
+ color: inherit;
+}
+
+.record-link:hover {
+ background: #f9f9f9;
+}
+
+.record-title {
+ font-weight: 500;
+ flex: 1;
+}
+
+.record-date {
+ font-size: 13px;
+ color: #888;
+ margin-left: 12px;
+}
+
+/* Record detail view */
+.record-detail {
+ padding: 20px 0;
+}
+
+.record-header {
+ margin-bottom: 16px;
+ padding-bottom: 16px;
+ border-bottom: 1px solid #eee;
+}
+
+.record-header .record-title {
+ font-size: 20px;
+ font-weight: 600;
+ margin-bottom: 8px;
+}
+
+.record-meta {
+ display: flex;
+ gap: 12px;
+ font-size: 13px;
+ color: #666;
+}
+
+.record-collection {
+ font-family: monospace;
+}
+
+.record-rkey {
+ font-family: monospace;
+ color: var(--btn-color);
+}
+
+.record-content {
+ margin-top: 16px;
+}
+
+.record-json {
+ background: #f5f5f5;
+ border-radius: 8px;
+ padding: 16px;
+ overflow-x: auto;
+ font-family: 'SF Mono', Monaco, monospace;
+ font-size: 13px;
+ line-height: 1.5;
+}
+
+/* Back navigation */
+.back-nav {
+ margin-top: 24px;
+}
+
+.back-nav a {
+ color: var(--btn-color);
+ text-decoration: none;
+}
+
+.back-nav a:hover {
+ text-decoration: underline;
+}
+
+/* Footer */
+.footer {
+ margin-top: 40px;
+ padding: 20px 0;
+ text-align: center;
+}
+
+.license {
+ display: flex;
+ justify-content: center;
+ margin-bottom: 16px;
+}
+
+.license-icon {
+ width: 24px;
+ height: 24px;
+ opacity: 0.6;
+ transition: opacity 0.2s;
+}
+
+.license-icon:hover {
+ opacity: 1;
+}
+
+.footer-content {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 8px;
+ font-size: 13px;
+ color: #888;
+}
+
+.footer-title {
+ font-weight: 500;
+}
+
+@media (prefers-color-scheme: dark) {
+ .browser-title {
+ color: #888;
+ }
+ .collection-btn {
+ background: #2a2a2a;
+ color: #e0e0e0;
+ }
+ .collection-btn:hover {
+ background: #3a3a3a;
+ }
+ .collection-item .collection-link {
+ background: #2a2a2a;
+ border-color: #444;
+ color: #e0e0e0;
+ }
+ .collection-item .collection-link:hover {
+ background: var(--btn-color);
+ border-color: var(--btn-color);
+ color: #fff;
+ }
+ .record-json {
+ background: #1a1a1a;
+ color: #d4d4d4;
+ }
+}
+
+/* Modal */
+.modal {
+ position: fixed;
+ inset: 0;
+ z-index: 1000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.modal[hidden] {
+ display: none;
+}
+
+.modal-backdrop {
+ position: absolute;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.5);
+}
+
+.modal-container {
+ position: relative;
+ width: 90%;
+ max-width: 800px;
+ max-height: 85vh;
+ background: #fff;
+ border-radius: 12px;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.modal-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 16px 20px;
+ border-bottom: 1px solid #eee;
+}
+
+.modal-header h2 {
+ font-size: 18px;
+ font-weight: 600;
+ margin: 0;
+}
+
+.modal-close {
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: none;
+ border: none;
+ font-size: 24px;
+ color: #666;
+ cursor: pointer;
+ border-radius: 6px;
+}
+
+.modal-close:hover {
+ background: #f0f0f0;
+ color: #333;
+}
+
+.modal-body {
+ flex: 1;
+ overflow-y: auto;
+ padding: 20px;
+}
+
+/* Browser tab button styling */
+button.tab {
+ background: none;
+ border: none;
+ cursor: pointer;
+ font-family: inherit;
+}
+
+/* Dark mode modal */
+@media (prefers-color-scheme: dark) {
+ .modal-container {
+ background: #1a1a1a;
+ }
+ .modal-header {
+ border-color: #333;
+ }
+ .modal-header h2 {
+ color: #e0e0e0;
+ }
+ .modal-close {
+ color: #888;
+ }
+ .modal-close:hover {
+ background: #333;
+ color: #e0e0e0;
+ }
+}
diff --git a/src/web/types.ts b/src/web/types.ts
new file mode 100644
index 0000000..8198271
--- /dev/null
+++ b/src/web/types.ts
@@ -0,0 +1,64 @@
+// Config types
+export interface AppConfig {
+ title: string
+ handle: string
+ collection: string
+ network: string
+ color: string
+ siteUrl: string
+}
+
+export interface Networks {
+ [domain: string]: {
+ plc: string
+ bsky: string
+ web: string
+ }
+}
+
+// ATProto types
+export interface DescribeRepo {
+ did: string
+ handle: string
+ collections: string[]
+}
+
+export interface Profile {
+ cid: string
+ uri: string
+ value: {
+ $type: string
+ avatar?: {
+ $type: string
+ mimeType: string
+ ref: { $link: string }
+ size: number
+ }
+ displayName?: string
+ description?: string
+ createdAt?: string
+ }
+}
+
+export interface Post {
+ cid: string
+ uri: string
+ value: {
+ $type: string
+ title: string
+ content: string
+ createdAt: string
+ lang?: string
+ translations?: {
+ [lang: string]: {
+ title: string
+ content: string
+ }
+ }
+ }
+}
+
+export interface ListRecordsResponse {
+ records: T[]
+ cursor?: string
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..f194c9e
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["src/web/*"]
+ }
+ },
+ "include": ["src/web/**/*.ts"]
+}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..8bf64c3
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,20 @@
+import { defineConfig } from 'vite'
+import { resolve } from 'path'
+
+export default defineConfig({
+ root: 'src/web',
+ publicDir: '../../public',
+ build: {
+ outDir: '../../dist',
+ emptyOutDir: true,
+ },
+ resolve: {
+ alias: {
+ '@': resolve(__dirname, 'src/web'),
+ },
+ },
+ server: {
+ host: '0.0.0.0',
+ port: 5173,
+ },
+})