Side A adds a complete, functional Reddit API client with rate limiting, typed response models, and robust error/status handling—new capability with real design decisions. Side B is purely cosmetic CSS polish (padding, colors, focus outlines) duplicated across two theme files, improving visual consistency but adding no functional or structural value to the codebase.
constitution · epochs · watch · epoch 3
c_b8e80699547c (tommy-mor) vs c_6864b1ca8ce6 (tommy-mor)
download prompt · raw event · cmp_d315ecdec8def5
council reasoning
A adds a full Reddit API client module (rate limiting, endpoints, typed serde models, and response helpers)—core reusable functionality. B only tweaks CSS for vote/pin controls and themes (padding, borders, focus rings), which is lasting polish but not architectural value.
Side A adds a new Reddit API client module with rate limiting, HTTP request handling, structured serde models for Reddit responses, custom deserializers, error handling, and helper methods for working with posts and comments. Side B is limited to CSS refinements for the voting and ontology UI (spacing, colors, focus states, layout, and theme overrides), which improve presentation and accessibility but do not add comparable core functionality.
sides
A — c_b8e80699547c (tommy-mor)
message
[432e1450] nice
diff preview
diff --git a/reddit.rs b/reddit.rs
new file mode 100644
index 0000000000000000000000000000000000000000..da27f6f76482dd27d44cad9d7b844972bf042e64
--- /dev/null
+++ b/reddit.rs
@@ -0,0 +1,420 @@
+use governor::{Quota, RateLimiter, Jitter};
+use nonzero_ext::nonzero;
+use reqwest::{Client, StatusCode};
+use serde::{Deserialize, Serialize};
+use std::sync::Arc;
+use std::time::Duration;
+use anyhow::{Result, Context, anyhow};
+
+/// Reddit API client with built-in rate limiting
+pub struct RedditClient {
+ client: Client,
+ limiter: Arc<governor::DefaultDirectRateLimiter>,
+ user_agent: String,
+}
+
+impl RedditClient {
+ /// Create a new Reddit client with rate limiting
+ /// Reddit API allows 60 requests per minute for OAuth2 authenticated apps
+ /// We'll be conservative and use 50 requests per minute
+ pub fn new() -> Self {
+ // Create rate limiter: 50 requests per minute
+ let quota = Quota::per_minute(nonzero!(50u32));
+ let limiter = Arc::new(RateLimiter::direct(quota));
+
+ // Create HTTP client with timeout
+ let client = Client::builder()
+ .timeout(Duration::from_secs(30))
+ .build()
+ .expect("Failed to create HTTP client");
+
+ // Reddit requires a unique user agent
+ let user_agent = format!(
+ "rust:sorter:v{} (by /u/hourLong_arnould)",
+ env!("CARGO_PKG_VERSION")
+ );
+
+ Self {
+ client,
+ limiter,
+ user_agent,
+ }
+ }
+
+ /// Fetch hot posts from a subreddit
+ pub async fn get_subreddit_hot(&self, subreddit: &str, limit: usize) -> Result<RedditListingResponse> {
+ // Wait for rate limiter
+ self.limiter.until_ready_with_jitter(Jitter::up_to(Duration::from_millis(100))).await;
+
+ let url = format!("https://www.reddit.com/r/{}/hot.json?limit={}", subreddit, limit);
+
+ let response = self.client
+ .get(&url)
+ .header("User-Agent", &self.user_agent)
+ .send()
+ .await
+ .context("Failed to send request to Reddit")?;
+
+ self.handle_response(response).await
+ }
+
+ /// Fetch top posts from a subreddit
+ pub async fn get_subreddit_top(&self, subreddit: &str, limit: usize, time_period: &str) -> Result<RedditListingResponse> {
+ self.limiter.until_ready_with_jitter(Jitter::up_to(Duration::from_millis(100))).await;
+
+ let url = format!(
+ "https://www.reddit.com/r/{}/top.json?limit={}&t={}",
+ subreddit, limit, time_period
+ );
+
+ let response = self.client
+ .get(&url)
+ .header("User-Agent", &self.user_agent)
+ .send()
+ .await
+ .context("Failed to send request to Reddit")?;
+
+ self.handle_response(response).await
+ }
+
+ /// Fetch new posts from a subreddit
+ pub async fn get_subreddit_new(&self, subreddit: &str, limit: usize) -> Result<RedditListingResponse> {
+ self.limiter.until_ready_with_jitter(Jitter::up_to(Duration::from_millis(100))).await;
+
+ let url = format!("https://www.reddit.com/r/{}/new.json?limit={}", subreddit, limit);
+
+ let response = self.client
+ .get(&url)
+ .header("User-Agent", &self.user_agent)
+ .send()
+ .await
+ .context("Failed to send request to Reddit")?;
+
+ self.handle_response(response).await
+ }
+
+ /// Fetch a specific post and its comments
+ pub async fn get_post(&self, subreddit: &str, post_id: &str) -> Result<Vec<RedditListingResponse>> {
+ self.limiter.until_ready_with_jitter(Jitter::up_to(Duration::from_millis(100))).await;
+
+ let url = format!(
+ "https://www.reddit.com/r/{}/comments/{}.json",
+ subreddit, post_id
+ );
+
+ let response = self.client
+ .get(&url)
+ .header("User-Agent", &self.user_agent)
+ .send()
+ .await
+ .context("Failed to send request to Reddit")?;
+
+ match response.status() {
+ StatusCode::OK => {
+ let listings: Vec<RedditListingResponse> = response
+ .json()
+ .await
+ .context("Failed to parse Reddit response")?;
+ Ok(listings)
+ }
+ StatusCode::TOO_MANY_REQUESTS => {
+ Err(anyhow!("Reddit rate limit exceeded. Please try again later."))
+ }
+ StatusCode::NOT_FOUND => {
+ Err(anyhow!("Post not found: r/{}/comments/{}", subreddit, post_id))
+ }
+ status => {
+ Err(anyhow!("Reddit API error: {}", status))
+ }
+ }
+ }
+
+ /// Fetch user profile (posts and comments)
+ pub async fn get_user_profile(&self, username: &str, content_type: &str, limit: usize) -> Result<RedditListingResponse> {
+ self.limiter.until_ready_with_jitter(Jitter::up_to(Duration::from_millis(100))).await;
+
+ let url = format!(
+ "https://www.reddit.com/user/{}/{}.json?limit={}",
+ username, content_type, limit
+ );
+
+ let response = self.client
+ .get(&url)
+ .header("User-Agent", &self.user_agent)
+ .send()
+ .await
+ .context("Failed to send request to Reddit")?;
+
+ self.handle_response(response).await
+ }
+
+ /// Handle Reddit API response with proper error checking
+ async fn handle_response(&self, response: reqwest::Response) -> Result<RedditListingResponse> {
+ match response.status() {
+ StatusCode::OK => {
+ let listing: RedditListingResponse = response
+ .json()
+ .await
+ .context("Failed to parse Reddit response")?;
+ Ok(listing)
+ }
+ StatusCode::TOO_MANY_REQUESTS => {
+ Err(anyhow!("Reddit rate limit exceeded. Please try again later."))
+ }
+ StatusCode::NOT_FOUND => {
+ Err(anyhow!("Reddit resource not found"))
+ }
+ StatusCode::FORBIDDEN => {
+ Err(anyhow!("Access forbidden. The subreddit may be private."))
+ }
+ status => {
+ Err(anyhow!("Reddit API error: {}", status))
+ }
+ }
+ }
+}
+
+// Reddit API response types
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct RedditListingResponse {
+ pub kind: String,
+ pub data: RedditListingData,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct RedditListingData {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub after: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub before: Option<String>,
+ pub children: Vec<RedditThing>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub modhash: Option<String>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct RedditThing {
+ pub kind: String,
+ pub data: RedditThingData,
+}
+
+/// Reddit's "Thing" data can be either a post, comment, or other types
+/// We use untagged enum to handle different data structures
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(untagged)]
+pub enum RedditThingData {
+ Post(RedditPost),
+ Comment(RedditComment),
+ // For things we don't care about yet (like "more" comments)
+ Other(serde_json::Value),
+}
+
+/// Reddit post data with serde handling all the parsing
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct RedditPost {
+ pub id: String,
+ pub name: String, // Full name (t3_xxx)
+ #[serde(default = "default_untitled")]
+ pub title: String,
+ #[serde(default = "default_deleted_user")]
+ pub author: String,
+ pub subreddit: String,
+ #[serde(default)]
+ pub url: String,
+ #[serde(default)]
+ pub permalink: String,
+ #[serde(default, deserialize_with = "deserialize_selftext")]
+ pub selftext: Option<String>,
+ #[serde(default)]
+ pub score: i64,
+ #[serde(default)]
+ pub num_comments: i64,
+ #[serde(default)]
+ pub created_utc: f64,
+ #[serde(default, deserialize_with = "deserialize_thumbnail")]
+ pub thumbnail: Option<String>,
+ #[serde(default)]
+ pub is_video: bool,
+ #[serde(default)]
+ pub is_self: bool,
+ // Additional useful fields
+ #[serde(default)]
+ pub ups: i64,
+ #[serde(default)]
+ pub downs: i64,
+ #[serde(default)]
+ pub upvote_ratio: f64,
+ #[serde(default)]
+ pub over_18: bool,
+ #[serde(default)]
+ pub spoiler: bool,
+ #[serde(default)]
+ pub stickied: bool,
+ #[serde(default)]
+ pub locked: bool,
+ #[serde(default)]
+ pub distinguished: Option<String>,
+}
+
+/// Reddit comment data
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct RedditComment {
+ pub id: String,
+ pub name: String, // Full name (t1_xxx)
+ #[serde(default = "default_deleted_user")]
+ pub author: String,
+ #[serde(default)]
+ pub body: String,
+ #[serde(default)]
+ pub score: i64,
+ #[serde(default)]
+ pub created_utc: f64,
+ #[serde(default)]
+ pub parent_id: String,
+ #[serde(default)]
+ pub permalink: String,
+ #[serde(default)]
+ pub depth: i32,
+ // Additional useful fields
+ #[serde(default)]
+ pub ups: i64,
+ #[serde(default)]
+ pub downs: i64,
+ #[serde(default)]
+ pub edited: RedditEditStatus,
+ #[serde(default)]
+ pub stickied: bool,
+ #[serde(default)]
+ pub distinguished: Option<String>,
+ #[serde(default)]
+ pub is_submitter: bool,
+ #[serde(default)]
+ pub collapsed: bool,
+ #[serde(default)]
+ pub controversiality: i32,
+}
+
+/// Reddit's edit status - can be false or a timestamp
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(untagged)]
+pub enum RedditEditStatus {
+ NotEdited(bool),
+ EditedAt(f64),
+}
+
+impl Default for RedditEditStatus {
+ fn default() -> Self {
+ RedditEditStatus::NotEdited(false)
+ }
+}
+
+// Helper functions for serde defaults
+fn default_untitled() -> String {
+ "Untitled".to_string()
+}
+
+fn default_deleted_user() -> String {
+ "[deleted]".to_string()
+}
+
+// Custom deserializer for selftext (empty strings should be None)
+fn deserialize_selftext<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
+where
+ D: serde::Deserializer<'de>,
+{
+ let s: Option<String> = Option::deserialize(deserializer)?;
+ Ok(s.filter(|text| !text.is_empty()))
+}
+
+// Custom deserializer for thumbnail (filter out "self", "default", empty)
+fn deserialize_thumbnail<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
+where
+ D: serde::Deserializer<'de>,
+{
+ let s: Option<String> = Option::deserialize(deserializer)?;
+ Ok(s.filter(|thumb| {
+ !thumb.is_empty() && thumb != "self" && thumb != "default" && thumb != "nsfw" && thumb != "spoiler"
+ }))
+}
+
+// Helper methods for extracting typed data from Things
+impl RedditThing {
+ /// Try to get this Thing as a Post
+ pub fn as_post(&self) -> Option<&RedditPost> {
+ if self.kind != "t3" {
+ return None;
+ }
+ match &self.data {
+ RedditThingData::Post(post) => Some(post),
+ _ => None,
+ }
+ }
+
+ /// Try to get this Thing as a Comment
+ pub fn as_comment(&self) -> Option<&RedditComment> {
+ if self.kind != "t1" {
+ return None;
+ }
+ match &self.data {
+ RedditThingData::Comment(comment) => Some(comment),
+ _ => None,
+ }
+ }
+
+ /// Check if th
… preview truncated; 1,666 characters omittedB — c_6864b1ca8ce6 (tommy-mor)
message
[51e73d38] voting looks better?
diff preview
diff --git a/server/static/theme_default.css b/server/static/theme_default.css
index 5f60ff7a8cf9045d8d43228baad8ba80ad057f2a..ec0fbe7acee0aa2802f978f14a9b0fc86e78c5b8 100644
--- a/server/static/theme_default.css
+++ b/server/static/theme_default.css
@@ -844,18 +844,25 @@ a.ont-vote-compare-btn {
color: var(--ui);
cursor: pointer;
font-size: 11px;
- padding: 2px 8px;
+ padding: 3px 10px;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 4px;
font-family: inherit;
+ border-radius: 2px;
}
button.ont-pin-btn:hover,
a.ont-vote-compare-btn:hover { color: var(--signal); }
+button.ont-pin-btn:focus-visible,
+a.ont-vote-compare-btn:focus-visible {
+ outline: 2px solid var(--link);
+ outline-offset: 2px;
+}
button.ont-pin-btn-active {
border-color: var(--link);
color: var(--signal);
+ background: color-mix(in srgb, var(--link) 12%, var(--g4));
}
.ont-garden-child-actions {
display: inline-flex;
@@ -868,12 +875,33 @@ a.ont-garden-vote-ico,
span.ont-garden-pinned-here {
font-size: 13px;
line-height: 1;
- padding: 0 2px;
- border: none;
- background: transparent;
+ padding: 2px 6px;
+ min-width: 26px;
+ min-height: 26px;
+ box-sizing: border-box;
+ border: var(--bv) solid;
+ border-color: var(--hi) var(--lo) var(--lo) var(--hi);
+ border-radius: 2px;
+ background: var(--g3);
cursor: pointer;
text-decoration: none;
color: inherit;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+}
+a.ont-garden-vote-ico:hover {
+ color: var(--signal);
+ background: var(--g4);
+}
+span.ont-garden-pinned-here {
+ border-color: var(--link);
+ color: var(--signal);
+ cursor: default;
+}
+button.ont-garden-pin-ico:focus-visible {
+ outline: 2px solid var(--link);
+ outline-offset: 2px;
}
body.view-ontology-light ol.ont-ranking-list li,
body.view-ontology-light ul.ont-group-list li {
@@ -889,8 +917,19 @@ body.view-ontology-light ul.ont-group-list li .item-link {
/* Vote compare page */
.vote-compare-shell {
- margin: 12px 0;
+ margin: 12px 0 20px;
max-width: 720px;
+ padding: 14px 16px 18px;
+ background: var(--g2);
+ border: var(--bv-lg) solid;
+ border-color: var(--hi) var(--lo) var(--lo) var(--hi);
+}
+body.view-vote-compare .vote-compare-shell > h2 {
+ margin-top: 0;
+ font-size: 12px;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ color: var(--meta);
}
.vote-compare-pair {
display: flex;
@@ -899,12 +938,20 @@ body.view-ontology-light ul.ont-group-list li .item-link {
gap: 10px 16px;
margin: 10px 0;
}
+.vote-compare-item {
+ text-decoration: none;
+}
.vote-compare-item code {
font-size: 13px;
}
+.vote-compare-item:hover code {
+ color: var(--signal);
+}
.vote-compare-vs {
color: var(--meta);
- font-size: 12px;
+ font-size: 11px;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
}
.vote-compare-slider-label {
display: flex;
@@ -917,7 +964,7 @@ body.view-ontology-light ul.ont-group-list li .item-link {
#vote-preference-slider {
flex: 1 1 180px;
min-width: 120px;
- accent-color: var(--ui);
+ accent-color: var(--link);
}
.vote-thread-picker {
margin: 10px 0;
@@ -931,9 +978,17 @@ body.view-ontology-light ul.ont-group-list li .item-link {
}
#vote-thread-select {
min-width: 160px;
+ background: var(--g3);
+ border: var(--bv) solid;
+ border-color: var(--hi) var(--lo) var(--lo) var(--hi);
+ color: var(--ui);
+ font-size: 12px;
+ padding: 3px 8px;
}
#vote-edge-history-region {
margin: 14px 0 18px;
+ padding-bottom: 8px;
+ border-bottom: 1px dashed var(--lo);
}
.vote-compare-preview-wrap {
margin: 12px 0;
@@ -946,8 +1001,37 @@ body.view-ontology-light ul.ont-group-list li .item-link {
#vote-compare-preview {
min-height: 48px;
}
+.vote-explain-label {
+ display: block;
+ font-size: 12px;
+ color: var(--meta);
+ margin: 12px 0 4px;
+}
+body.view-vote-compare #vote-explain {
+ width: 100%;
+ max-width: 100%;
+ box-sizing: border-box;
+ background: var(--g3);
+ border: var(--bv) solid;
+ border-color: var(--hi) var(--lo) var(--lo) var(--hi);
+ color: var(--prose);
+ font: inherit;
+ line-height: 1.45;
+ padding: 8px 10px;
+}
+body.view-vote-compare #vote-explain:focus {
+ outline: none;
+ border-color: var(--link);
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--link) 22%, transparent);
+}
+body.view-vote-compare #vote-compare-form button[type="submit"] {
+ margin-top: 6px;
+}
.vote-edge-history-title {
- font-size: 13px;
+ font-size: 12px;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ color: var(--meta);
margin: 16px 0 8px;
}
ol.vote-edge-history {
@@ -968,6 +1052,8 @@ li.vote-edge-history-row {
.vote-edge-bar {
margin-top: 4px;
max-width: 100%;
+ border-radius: 2px;
+ overflow: hidden;
}
.vote-edge-reason {
margin-top: 4px;
diff --git a/server/static/theme_retro_craft.css b/server/static/theme_retro_craft.css
index 3853e9edaf4c77c62dcf350ff28920448dd943a8..092d4b3540b052cc86870179e4c264541c4446cc 100644
--- a/server/static/theme_retro_craft.css
+++ b/server/static/theme_retro_craft.css
@@ -587,3 +587,337 @@ body.view-ontology #controls {
padding-right: 1.25rem;
}
}
+
+/* ----------------------------------------------------------------
+ Ontology garden: pin HUD, row pins, vote-compare
+ (Global craft `code` + `button[type=submit]` are tuned for dark
+ thread pages; these overrides match the cream ontology shell.)
+ ---------------------------------------------------------------- */
+body.view-ontology #slug-pin-hud.slug-pin-hud {
+ margin-left: auto;
+ max-width: min(42vw, 280px);
+ font-family: var(--font-ui);
+ font-size: 0.72rem;
+ color: #5c574e;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+body.view-ontology .slug-pin-hud-link {
+ color: #1a4a8c;
+ text-decoration: none;
+ display: inline-flex;
+ align-items: center;
+ gap: 0.25rem;
+}
+body.view-ontology .slug-pin-hud-link:hover {
+ color: #0d3d82;
+ text-decoration: underline;
+}
+body.view-ontology .slug-pin-hud-glyph {
+ font-size: 0.95rem;
+ line-height: 1;
+}
+
+body.view-ontology .ont-item-meta {
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.35rem;
+}
+body.view-ontology .ont-item-title {
+ flex: 1 1 auto;
+ min-width: 0;
+}
+body.view-ontology .ont-item-pin-zone {
+ flex: 0 0 auto;
+ margin-left: auto;
+ display: flex;
+ align-items: center;
+ gap: 0.4rem;
+}
+
+body.view-ontology button.ont-pin-btn,
+body.view-ontology a.ont-vote-compare-btn {
+ background: #ebe6dc;
+ border: 1px solid #c8c4bc;
+ border-radius: 2px;
+ color: #3d3a34;
+ cursor: pointer;
+ font-family: var(--font-ui);
+ font-size: 0.72rem;
+ letter-spacing: 0.04em;
+ padding: 0.25rem 0.55rem;
+ text-decoration: none;
+ display: inline-flex;
+ align-items: center;
+ gap: 0.3rem;
+}
+body.view-ontology button.ont-pin-btn:hover,
+body.view-ontology a.ont-vote-compare-btn:hover {
+ border-color: #a68e6b;
+ color: #1a1814;
+}
+body.view-ontology button.ont-pin-btn-active {
+ border-color: #1a4a8c;
+ background: color-mix(in srgb, #1a4a8c 12%, #ebe6dc);
+ color: #0d2d5c;
+}
+body.view-ontology button.ont-pin-btn:focus-visible,
+body.view-ontology a.ont-vote-compare-btn:focus-visible {
+ outline: 2px solid #1a4a8c;
+ outline-offset: 2px;
+}
+
+body.view-ontology .ont-garden-child-actions {
+ display: inline-flex;
+ align-items: center;
+ margin-right: 0.35rem;
+ vertical-align: middle;
+ gap: 0.1rem;
+}
+body.view-ontology button.ont-garden-pin-ico,
+body.view-ontology a.ont-garden-vote-ico,
+body.view-ontology span.ont-garden-pinned-here {
+ font-size: 0.95rem;
+ line-height: 1;
+ padding: 0.1rem 0.35rem;
+ min-width: 1.65rem;
+ min-height: 1.65rem;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border: 1px solid #c8c4bc;
+ border-radius: 2px;
+ background: #f7f3eb;
+ cursor: pointer;
+ text-decoration: none;
+ color: #1a1814;
+}
+body.view-ontology a.ont-garden-vote-ico:hover {
+ border-color: #a68e6b;
+ background: #ebe6dc;
+}
+body.view-ontology span.ont-garden-pinned-here {
+ border-color: #1a4a8c;
+ background: color-mix(in srgb, #1a4a8c 10%, #f7f3eb);
+ cursor: default;
+}
+body.view-ontology form.ont-garden-pin-form {
+ display: inline;
+ margin: 0;
+ padding: 0;
+}
+body.view-ontology button.ont-garden-pin-ico:focus-visible {
+ outline: 2px solid #1a4a8c;
+ outline-offset: 2px;
+}
+
+body.view-ontology ol.ont-ranking-list li,
+body.view-ontology ul.ont-group-list li {
+ display: flex;
+ align-items: baseline;
+ gap: 0.35rem;
+}
+body.view-ontology ol.ont-ranking-list li .item-link,
+body.view-ontology ul.ont-group-list li .item-link {
+ flex: 1;
+ min-width: 0;
+}
+
+body.view-ontology code,
+body.view-ontology .vote-compare-item code,
+body.view-ontology .vote-edge-meta code {
+ background: #ebe6dc;
+ color: #1a1814;
+ border: 1px solid #d4cfc4;
+ padding: 0.12em 0.35em;
+ font-size: 0.88em;
+}
+
+body.view-ontology div.ratio-bar {
+ background: #e3ded4;
+ border: 1px solid #c8c4bc;
+}
+body.view-ontology div.ratio-left,
+body.view-ontology div.ratio-right {
+ background: #cec9bf;
+}
+body.view-ontology div.ratio-left.current,
+body.view-ontology div.ratio-right.current {
+ background: #9a7b4a;
+}
+
+body.view-ontology.view-vote-compare .vote-compare-shell {
+ margin: 0.75rem 0 1.5rem;
+ max-width: 36rem;
+ padding: 0.85rem 1rem 1.1rem;
+ background: #f7f3eb;
+ border: 1px solid #c8c4bc;
+ border-radius: 3px;
+ box-shadow: 0 1px 0 rgba(26, 24, 20, 0.06);
+}
+body.view-ontology.view-vote-compare .vote-compare-shell > h2 {
+ margin-top: 0;
+ font-size: 0.7rem;
+ letter-spacing: 0.16em;
+ text-transform: uppercase;
+ color: #5c574e;
+}
+body.view-ontology .vote-compare-pair {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.65rem 1rem;
+ margin: 0.65rem 0 0.85rem;
+}
+body.view-ontology .vote-compare-vs {
+ color: #8a857a;
+ font-family: var(--font-ui);
+ font-size: 0.72rem;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+}
+body.view-ontology .vote-compare-item {
+ text-decoration: none;
+}
+body.view-ontology .vote-compare-item:hover code {
+ border-color: #a68e6b;
+ background: #f0ebe3;
+}
+
+body.view-ontology .vote-thread-picker {
+ margin: 0.85rem 0;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.5rem 0.75rem;
+}
+body.view-ontology .vote-thread-picker-label {
+ font-family: var(--font-ui);
+ font-size: 0.72rem;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: #5c574e;
+}
+body.view-ontology #vote-thread-select {
+ min-width: 10rem;
+ background: #f7f3eb;
+ border: 1px solid #c8c4bc;
+ color: #1a1814;
+ font-family: var(--font-ui);
+ font-size: 0.78rem;
+ padding: 0.3rem 0.45rem;
+}
+
+body.view-ontology .vote-compare-slider-label {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.5rem 0.65rem;
+ margin: 0.85rem 0;
+ font-family: var(--font-ui);
+ font-size: 0.72rem;
+ color: #5c574e;
+}
+body.view-ontology #vote-preference-slider {
+ flex: 1 1 11rem;
+ min-width: 8rem;
+ accent-color: #9a7b4a;
+}
+
+body.view-ontology .vote-explain-label {
+ display: block;
+ font-family: var(--font-ui);
+ font-size: 0.72rem;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: #5c574e;
+ margin: 0.65rem 0 0.35rem;
+}
+body.view-ontology #vote-compare-form textarea,
+body.view-ontology.view-vote-compare textarea#vote-explain {
+ background: #fdfcfa;
+ border: 1px solid #c8c4bc;
+ color: #1a1814;
+ font-family: var(--font-prose);
+ font-size: 0.9rem;
+ line-height: 1.45;
+ width: 100%;
+ max-width: 100%;
+ padding: 0.55rem 0.65rem;
+ margin-top: 0.25rem;
+}
+body.view-ontology #vote-compare-form textarea:focus {
+ border-color: #a68e6b;
+ outline: none;
+ box-shadow: 0 0 0 2px color-mix(in srgb, #a68e6b 28%, transparent);
+}
+
+body.view-ontology #vote-compare-form button[type="s
… preview truncated; 1,584 characters omittedHardlinks — judgments / attempts / prompt
judgments
attempts
Prompt text is loaded only by the download route.