diff --git a/.achievement_data/.gitkeep b/.achievement_data/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/.achievement_data/.htaccess b/.achievement_data/.htaccess
new file mode 100644
index 0000000..fa39801
--- /dev/null
+++ b/.achievement_data/.htaccess
@@ -0,0 +1,6 @@
+
+ Require all denied
+
+
+ Deny from all
+
diff --git a/.htaccess b/.htaccess
index fc13343..1a8ccd5 100644
--- a/.htaccess
+++ b/.htaccess
@@ -8,8 +8,14 @@
ExpiresByType image/webp "access plus 30 days"
ExpiresByType audio/mpeg "access plus 30 days"
ExpiresByType audio/wav "access plus 30 days"
+
+
+ Header set Cache-Control "no-store, no-cache, must-revalidate, max-age=0"
+ Header set Pragma "no-cache"
+ Header set Expires "0"
+
Header set Cache-Control "no-cache, must-revalidate"
Header set Pragma "no-cache"
diff --git a/AI_MAP.md b/AI_MAP.md
index 9b007a5..ace542d 100644
--- a/AI_MAP.md
+++ b/AI_MAP.md
@@ -94,3 +94,12 @@ This section records current boundaries only. Version-by-version cleanup history
## v39.16.31 routing notes
- Hair Trigger preset: `js/world_reset_presets.js`; physics tool icon renderer: `js/ui_tools.js`; world tooltip offset: `js/render.js`.
+
+
+## Achievement system (v39.16.56)
+- `js/achievements.js`: 51 definitions, local unlock state, persistent placement/ant-kill/reproduction/fight/Undo/Redo/quick-delete progress, field-scoped direct-feed/cleaner/self-sufficiency/enemy-ant progress, individual care/hold/temperature/fight markers, allowlisted inline disclosures and local progress, shared-rate bars/counts, event-driven generation/status-effect/first-aid/plushie checks, completionist chaining, cached hidden-dialog rendering, debounced pending synchronization, and periodic additive full-state recovery synchronization.
+- `achievement_api.php`: validates current IDs, reads legacy verbose or compact v2 aggregate state, writes compact indexed arrays, migrates the intersection of the retired automation/link achievements into `mechanized_industry`, accepts idempotent bulk unlock synchronization, and returns anonymous global percentages plus achiever counts.
+- `css/components.css`: achievement rows use `--achievement-rate` to render horizontal background fills; unlocked rows use a distinct fill.
+- `js/world.js`, `js/snapshot_system.js`, and `js/save_codec.js`: field-local achievement counters and per-entity/item markers reset with a new field and persist within current-format saves; the current save schema intentionally has no backward compatibility.
+- Hooks span placement/deletion/history, robot cleaning, direct care and consumables, fight starts/deaths, ant damage attribution, save slots, pause input, hold state, population/temperature/weather evaluation, first-aid recovery, shared poke targeting/highlights, plushie fling, and nest-box/pipe weather blocking.
+- Audits: `scripts/achievement_audit.js` checks legacy/current boundaries and recovery; `scripts/playstyle_achievement_audit.js` checks the play-style conditions, reset scopes, false-positive guards, tooltips, and production hooks; `scripts/achievement_integration_audit.py` checks IDs and integration wiring.
\ No newline at end of file
diff --git a/BUILD_NOTE.txt b/BUILD_NOTE.txt
index 8d18765..47d5802 100644
--- a/BUILD_NOTE.txt
+++ b/BUILD_NOTE.txt
@@ -6,7 +6,7 @@ tarinai_compact_save_stage4
- Deflate state is folded into the low bit of the frame-length varuint, eliminating the separate compression byte.
- 15-bit text conversion uses arithmetic Unicode range mapping instead of a 32768-character string and reverse Map.
- Previous binary schemas are intentionally unsupported.
-- Version: 39.16.31
+- Version: 39.16.56
- CompressionStream and DecompressionStream are drained concurrently to avoid browser backpressure stalls.
- Load confirmation uses the in-game dialog; fan wind is realtime; zunchi tool preview opacity, carried plushie depth, and blanket grass rules were corrected.
@@ -30,13 +30,13 @@ tarinai_compact_save_stage4
- v39.16.19: added direct food/medicine giving by placement or pinched drop, with a give overlay, stronger food-gift personality change, and heart bursts; removed unused collision reasons and duplicate constraint projection dirty checks.
-- v39.16.20: localized the direct-feeding personality reason, normalized electrocution deaths to 「感電」, added detector-controlled gate fences, made chart legend labels fully clickable, and escaped non-ASCII runtime source.
+- v39.16.20: localized the direct-feeding personality reason, normalized electrocution deaths to the Japanese electrocution label, added detector-controlled gate fences, made chart legend labels fully clickable, and escaped non-ASCII runtime source.
- v39.16.21: hid pinch overlays for rope/rod/spring/wire connection items and added detector time schedules with a dual-thumb range control, overnight ranges, and persistent copy/save state.
- v39.16.22: shortened detector copy, removed the time-range explanatory hint, cascaded connection-item removal when any endpoint disappears, and repaired detached plushies so they return to a living owner or disappear with a missing owner.
-- v39.16.23: excluded transient splat effects from all connection endpoint selection and renamed the user-facing feeding need from 「摂餌」 to 「食欲」.
+- v39.16.23: excluded transient splat effects from all connection endpoint selection and renamed the user-facing feeding need from the formal feeding term to the appetite label.
- v39.16.25: added toilet-seeking before normal defecation; renamed rain shelter to parasol; added sunny +3.5C and unsheltered rain -3.5C felt-temperature corrections; parasols neutralize both and block sunbathing. Pet food, first aid, and sedative remain included.
- v39.16.29: replaced toilet-seeking with walkable toilet sand that immediately removes poop spawned on its surface; added a parasol ground shadow and explicit hot-sun/cold-rain temperature-seeking targets; fixed null candidate temperatures being interpreted as 0C.
@@ -46,3 +46,42 @@ tarinai_compact_save_stage4
- v39.16.30: added a few bounce fences to War, reduced ordinary-fence restitution, restored ball collisions for visible sleeping Tarinai, moved nest-box tooltips closer, allowed movable-item placement overlap, and fixed delete-tool target lookup.
- v39.16.31: unified in-world tooltip spacing, added the Hair Trigger preset, and replaced physics-category line-like tool icons with distinct semantic silhouettes.
+
+- v39.16.35: moved snap-mode switching to a dedicated operation control with Free/20px/40px/60px cycling, removed F/G from size badges, allowed valid placement against the field top edge, and standardized preset descriptions with Japanese full stops.
+
+- v39.16.37: unified biological/environmental conditions into the detector, removed the standalone sensor and relay types with no save migration, folded temperature/limits, moved secondary controls into the header menu, preserved tool mode while browsing tarinai data, and added five-second mobile selection tips.
+
+- v39.16.38: detector numeric sliders match the time-control presentation; colony settings are unified; display settings are inline; meter colors, snap control, range-delete icon, and ant chart defaults are revised.
+
+- v39.16.39: restored the compact sticky operation palette, added exact-match panel search, reorganized colony settings/stats, and refreshed range-delete and mechanism icons.
+- v39.16.40: moved partial panel search to the header with previous/next navigation and tooltip text indexing; localized snap mode labels; simplified bounce-fence and reciprocator icons; matched expanded colony settings to the chart background.
+
+39.16.41: Added local achievements and optional self-hosted shared unlock percentage API.
+
+39.16.42: Expanded achievements to ten conditions, added local/shared reset, rapid-event windows, generation streaks, and special collision tracking.
+
+39.16.44: Expanded achievements to fifteen conditions, changed rapid deaths to 50 in 10 seconds, moved conditions into game tooltips, added toast-to-button absorption, and added colony disease/happiness, cumulative hand feeding, ritual ignition, and laxative-starvation achievements.
+
+39.16.44: Audited all achievements; fixed impossible slave generation tracking, persisted generation runs, queued simultaneous unlock toasts, hardened launched-zunchi stop detection, and preserved laxative-at-death timing.
+
+39.16.45: Added seven play-style achievements for passive observation, fifty world placements, detector automation, rope/rod/spring coverage, a five-minute death-free 25-Tarinai colony, 25 live ants, and 30 cumulative ant kills; persisted world-scoped timers and placement progress.
+
+39.16.46: Revised achievement naming and disclosure UI, replaced achievement tooltips with inline post-unlock descriptions and horizontal shared-rate bars, merged detector automation plus link coverage into 機械化産業, changed placement and ant-kill goals to persistent totals of 100, and made direct-feeding progress reset with the field.
+
+39.16.47: Added 大いなる母 and 喧嘩両成敗, renamed five achievements, revised five revealed descriptions, and added a debug-only all-achievements button.
+
+39.16.48: Corrected the zunchi-slave fight-record threshold text to 30%; fixed wire placement totals, danger-kill attribution for 喧嘩両成敗, partial legacy link migration, strict player-ID validation, post-unlock storage writes, and debug-only unlock isolation from shared statistics.
+
+39.16.49: Added additive client-to-server full-state synchronization for automatic aggregate recovery after server-side achievement data loss.
+
+39.16.51: Expanded achievements from 27 to 43 with history, cleanup, care, ecology, temperature, weather, medicine, combat, and ant-experiment conditions; persisted the required reset-scoped metadata in a new incompatible save schema; made nest-box and pipe occupants weather-protected; and added false-positive guards for time-zero markers and stale ant damage attribution.
+
+39.16.52: Reorganized achievements into four collapsible categories, removed achievement tooltips, surfaced pre-unlock disclosures in the achievement UI, and replaced native browser confirmations with a shared in-game dialog.
+
+
+39.16.53: Renamed the four achievement categories to 生態/実験/建築/その他, moved unlocked achievements to the top of their own categories, disclosed four additional conditions before unlock, raised 大いなる母 to 3333 births, and moved achievement unlock toasts to the upper-left.
+
+39.16.54: Added 真・たりない観察, 悠久の歴史, 五体満足, and 零体満足; disclosed 悠久の歴史 before unlock; reduced achievement overhead by skipping closed-dialog list rebuilds, caching category mappings, debouncing pending sync, and retaining periodic full recovery sync.
+
+39.16.55: Added 町のお医者さん and 割とふわふわ; changed ベビーブーム to 30 births per minute and 管理された楽園 to one uninterrupted happy in-game day from play day 6; repaired shared つつく targeting, damage, plushie fling, and hover overlay; corrected manual rotator/reciprocator push direction; tightened tool/limit typography; reduced periodic achievement scans; and changed server aggregate writes to compact v2 JSON with legacy-state migration.
+39.16.56: Added 混沌ヲ希求スル者 and 潔癖症; made fight progress persistent; revised ミニマリスト, おくすり手帳全埋め, and 清掃業者; and made achievement-rate bars visible on unlocked cards.
diff --git a/FILES.json b/FILES.json
index f1f8860..d3b6de5 100644
--- a/FILES.json
+++ b/FILES.json
@@ -1,6 +1,6 @@
{
"purpose": "Token-efficient machine-readable routing for tarinai_. Use scripts/ai_inventory.py for exact current file expansion.",
- "version": "39.16.31",
+ "version": "39.16.56",
"entrypoints": {
"app": "index.html",
"runtime": "js/main.js",
@@ -37,7 +37,9 @@
"AI_MAP.md",
"GAME_FEATURES.md",
"FILES.json",
- "service-worker.js"
+ "service-worker.js",
+ "achievement_api.php",
+ ".achievement_data/.htaccess"
],
"meaning": "Repo/app shell metadata, server/static cache config, docs, manifest, entrypoint."
},
@@ -149,7 +151,8 @@
"js/ui*.js",
"js/text_catalog.js",
"js/command_dispatcher.js",
- "js/family_graph.js"
+ "js/family_graph.js",
+ "js/achievements.js"
],
"meaning": "DOM UI, input, tools, selected panel, logs, charts, family tree, text labels, command routing."
},
diff --git a/GAME_FEATURES.md b/GAME_FEATURES.md
index 38bd88e..fb48893 100644
--- a/GAME_FEATURES.md
+++ b/GAME_FEATURES.md
@@ -1,340 +1,86 @@
-## v39.16.17 mechanical one-way fence runtime fix
-
-- Mechanical objects now query their canonical physics-body velocity when resolving one-way-fence contacts.
-- This removes the `bodyFor is not defined` runtime failure while preserving the existing directional fence behavior.
-
-## v39.16.16 directional fences, chart controls, and living wire endpoints
-
-- Tarinai can physically cross one-way fences in the arrow direction, including swept/high-speed movement.
-- Colony chart color dots toggle individual series.
-- Wire and insulated wire can attach to Tarinai and ants. Energized living endpoints take damage and panic regardless of insulation; only bare wire damages incidental contact.
-- Selected Tarinai data shows `感電している。` while shocked.
-
-
-## v39.16.16 colony lineage and directional routing
-
-- Population history overlays interval birth and death counts.
-- Rotators preserve negative motor speed and rotate in reverse.
-- Tarinai Champion boosts to aggression, openness, and sociability pass to descendants with generational damping.
-- Champion-pair mating preference is slightly reduced.
-- One-way fence placement previews show the allowed direction.
-- Pathfinding treats one-way fences as passable only along their arrow direction.
-
-## v39.16.16 colony graph, reverse rotator, and champion lineage
-
-- コロニー統計の個体数グラフへ、2時間区間ごとの誕生数と死亡数を追加した。
-- 回転体のモーター速度は負値を保持し、負の角度方向へ逆回転できる。
-- たりない王の攻撃性・開放性・社交性補正は子孫の誕生時性格へ減衰継承される。
-- たりない王同士の繁殖優先倍率を5.0から4.0へ下げ、繁殖確率をやや抑えた。
-- 保存形式は変更しない。
-
# tarinai_ Game Features
-## v39.16.16 link, bounce-fence, and spring fixes
+`tarinai_` is an observation sandbox about small creatures called tarinai. The player shapes a field with food, medicine, furniture, hazards, machines, weather, and ground choices, then watches how the colony reacts. The game is about cause and effect: small changes in the environment can alter movement, stress, disease, relationships, reproduction, accidents, cleanup, and survival.
-- ヒモ、棒、バネ、電線、絶縁電線は、接続線が画面内を横切る場合、中点の空間セルが画面外でも描画される。
-- バウンス柵はたりないを外力インパルスで確実に吹き飛ばし、柵自身が作る急激な速度変化では衝撃ダメージを与えない。
-- バネは受動式のまま剛性を高め、減衰と強制位置補正を下げ、より大きく長く往復する。
+## Core Play
-## v39.16.13 detector and fence changes
+The player starts by choosing a field and ground, then places objects or uses direct tools on the colony. Tarinai continue acting on their own while the player observes, edits the environment, removes problems, or creates new ones. The colony panel, event log, selected-tarinai data, and family tree help explain what is happening without taking control away from the simulation.
-- 感圧スイッチを「検知器」へ改名し、道具UI上で通常の柵と位置を入れ替えた。
-- 検知器は軸平行の矩形範囲で対象を数えるほか、時刻も検知できる。専用UIから対象・必要数・範囲、または二つまみスライダーによる作動時間帯を設定する。
-- 柵の配置境界判定を実際の細長い形状に合わせ、フィールド上下端近くへ配置できるようにした。
-- バウンス柵の反発係数、最低射出速度、最大速度を引き上げ、衝突直後に強く跳ねるようにした。
-- ワープゲートを道具、描画、更新、信号、保存形式から削除した。
-- 一方通行柵を追加した。矢印方向には通過でき、逆方向のみ衝突する。
-- バイナリスキーマ36、スナップショット30。旧形式との読み込み互換は持たない。
+The game supports quiet observation and chaotic experiments. A player can build a comfortable habitat with beds, food, grass, and cleaning tools, or create a dangerous machine field with rotators, fences, stones, pins, explosions, ants, and poison.
-## v39.16.11 mechanism fixes and passive spring
+## Tarinai
-- 検知器は矩形範囲内の「たりない」または「アイテム」を数えるか、設定した時刻帯に入ると電線へ信号を送る。開始時刻が終了時刻より後なら日付をまたぎ、同時刻なら終日作動する。
-- 一方通行柵は矢印方向だけ通過でき、逆方向のたりない・アイテム・機構を遮る。
-- バウンス柵は衝突対象へ強い即時反発を与える。
-- バネは棒と同じ2点接続方式で設置する。自動伸縮はせず、外力による伸び縮みに対して受動的に反発し、弾性運動する。
-- 電線と絶縁電線はアイテム同士を接続し、複数本を経由して信号を伝える。通常電線は通電中に接触したたりないへダメージを与え、絶縁電線は与えない。
-- ヒモ、棒、バネ、電線、絶縁電線は、いずれかの接続先が消滅すると連鎖的に消滅する。
-- 所有者が生存しているぬいぐるみは、投擲中やプレイヤー所持中を除き頭上へ復帰し、地面に放置されない。
-- バイナリスキーマ34、スナップショット28。v39.16.10と保存形式は同じ。
+Tarinai are autonomous creatures with HP, needs, stress, personality, age, family links, diseases, item effects, and behavior states. They choose actions from their current condition and surroundings instead of following a fixed script. Hunger can make them seek grass or food, sleepiness can make them find a bed, fear can make them flee, and social state can make them follow, fight, avoid, or approach another tarinai.
-## v39.16.9 load UI and item behavior fixes
+Each tarinai has individual tendencies. Aggressive tarinai fight more easily, social tarinai seek others more often, nervous tarinai react strongly to danger, and inherited traits can make children resemble their parents. Selecting a tarinai shows health, needs, stress, effects, relations, inventory, family information, and the reason for its current behavior.
-- Replaced browser-native load confirmations with an in-game confirmation window.
-- Kept fans in the realtime scheduler lane so wind applies continuously instead of as periodic gusts.
-- Made the zunchi tool preview use the fresh, fully opaque stage.
-- Rendered carried plushies at the same depth position as their owner.
-- Disabled grass spawning, growth, placement capacity, and existing grass on the fluffy blanket ground.
+Tarinai can be damaged by hazards, pushed by physics objects, carried by ants, attached to links, blocked by fences, affected by medicine, frightened by accidents, comforted by habitat items, or preserved when outside the active field. Some objects can become emotionally important, so losing them may cause panic.
-## v39.16.8 compact amount, relationship, seed, and framing save
+## Food And Consumables
-- アイテム種類の標準amountは保存せず、標準値と異なる共通値または個別差分だけを保存する。共通値候補は実バイト数で選択する。
-- 個体関係の対象インデックスを昇順に並べ、前の対象との差分として保存する。
-- worldSeedを固定16進64bitへ統一し、バイナリ上では8バイト固定で保存する。
-- バイナリ先頭はスキーマ1バイトへ統合し、上位bitで文字列テーブルの有無を示す。空の文字列テーブルと重複したスナップショット版番号は保存しない。
-- 圧縮フラグをフレーム長の下位bitへ統合し、独立した1バイトを削除する。
-- 15bit文字化の32768文字テーブルと逆引きMapを廃止し、Unicodeコードポイントの算術変換へ変更する。
-- バイナリスキーマ33、スナップショット27。旧形式との読み込み互換は持たない。
-
-## v39.16.7 column-oriented creature save and packed item flags
+Grass is both food and habitat material. Tarinai can eat it, feel comfort near it, gather it for simple beds, and interact with it through ground and weather systems. Water can be drunk, can appear from rain, and can be generated or copied by tools.
-- 個体の固定情報を項目別の列指向ビットプレーンへ変更し、全個体で0の列は保存しない。
-- 睡眠状態、遺伝差分、戦績差分の存在は各ID・差分マスクから直接判定し、重複していた3bit/個体を削除する。
-- アイテムの共通追加状態、amount差分、追加状態差分のフラグを全ブロック共通の連続ビット列へ格納する。
-- 共通追加状態は候補ごとに専用バイナリを試算し、全件個別保存より実際に小さい場合だけ採用する。
-- バイナリスキーマ32、スナップショット26。旧形式との読み込み互換は持たない。
-
-## v39.16.4 compact identity and normal-item binary save
-
-- 個体のbirthSeed文字列を保存せず、出生番号と32bitハッシュへ分解する。`worldSeed + birthSerial`で再現できる個体はハッシュも省略する。
-- 新規出生のbirthSeedは親文字列を含めず、親子関係と遺伝情報は既存の専用フィールドで保持する。
-- 通常アイテムの追加情報を型別専用バイナリへ移行し、JSONテキストを保存しない。未知の追加配列も数値・文字列の型付きバイナリで保存する。
-- 同じ種類のアイテムはamountの最頻値をブロックに1回だけ保存し、同一追加状態が複数ある場合も共有する。個別アイテムには差分だけを保存する。
-- バイナリスキーマ30、スナップショット24。旧形式との読み込み互換は持たない。
-
-## v39.16.3 physics binary save and reciprocator cargo contacts
-
-- 回転体・毒ブロック・往復体・ロープ・ロッドの追加情報を、JSON文字列ではなく専用バイナリで保存する。型名、キー名、デフォルト値、重複座標、標準輪郭、インデックス参照済み接続先の文字列情報を省略する。
-- バイナリスキーマは29。v39.16.2のスキーマ28は読み込み互換を維持する。
-- 往復体はボール、石、画鋲などの可動物理アイテムを押し出すが、それらとの接触では方向、レール位相、モーター速度を変更しない。
-
-## v39.16.2 reciprocator collision and placement alignment
-
-- 往復体は、進行方向にある機構、静的障害物、物理アイテムへ衝突した時点で方向転換する。接触分離によって重なりが解消されても反転判定を失わない。
-- 静的障害物との判定から往復体を除外しない。高速移動時は障害物付近でサブステップ判定を増やし、すり抜けを抑える。
-- 往復体の設置前オーバーレイと実配置は同じ角度初期化処理を使用し、物理ボディー角度とレール軸を同じ順序で確定する。
-- セーブ形式は変更しない。
-
-## v39.16.1 debug save-size breakdown
-
-- `?debug=1` でセーブまたは呪文出力を実行すると、デバッグオーバーレイにバイナリ内訳、文字数、エンコード時間が表示される。
-- 内訳は形式情報、文字列テーブル、ワールド、個体基本情報、個体シード、個体関係、アイテム索引、通常アイテム、物理アイテムに分けて実占有バイトを集計する。
-- 通常アイテム・物理アイテムの追加情報について、バイナリ内の占有量とJSON UTF-8元サイズを併記する。ロード後は直近のデコード時間も表示する。
-
-This document describes the playable features of `tarinai_`: what the player sees, what tools do, and how tarinai behave. It is intentionally player/design-facing, not an implementation map.
-
-## v39.16.0 mechanical interaction and limit audit
-
-- 回転体・往復体・毒ブロックは、双方の「当たり判定ON」時にSAT矩形判定で相互作用する。当たり判定OFFの機構は機構同士の衝突対象から除外される。
-- 3種類の輪郭編集は20px間隔のグリッド格子点へスナップする。直線、自由描画、消去位置、テンプレート、再編集時の既存端点に同じ規則を適用する。
-- コロニー上限は中央生成APIで常時適用され、通常実行時に上限を迂回するオプションは持たない。既存要素やセーブ復元要素は削除せず、その後の新規生成のみを抑止する。
-
-## v39.15.165 behavior and colony controls
-
-- たりない王は、繁殖可能な別のたりない王を広い範囲から優先して探し、通常より強い繁殖欲求と短い再繁殖待機時間を持つ。
-- 往復体の新規配置時の標準方向は90度。
-- 毒ブロックの新規配置時の標準輪郭は小型(半径44、輪郭104×40、太さ11)。
-- コロニーパネルで、たりない個体数と生存オブジェクト総数の上限を設定できる。空欄または0は上限なし。既存要素は削除せず、新規追加だけを抑止する。
-- 上限値はスナップショットとバイナリセーブに保存される。
-
-## Game Overview
-
-`tarinai_` is an observation sandbox about small creatures called tarinai. The player places food, furniture, hazards, machines, and environment tools, then watches how individuals live, move, eat, sleep, panic, fight, reproduce, get sick, recover, and leave family records.
-
-The core loop is:
-
-1. Choose a field and observe the colony.
-2. Place tools or objects that change the environment.
-3. Select individual tarinai to inspect health, needs, personality, relations, diseases, records, and current behavior.
-4. Watch the event log, colony graphs, and family tree to understand long-term outcomes.
-
-## Main UI
-
-- **Canvas / field**: the main simulation area. Tarinai, items, effects, ants, weather, and terrain are drawn here.
-- **Top buttons**: pause/resume, speed control, ecology guide, sound settings, credits, save, display settings, and reset.
-- **Display settings**: manually adjust effects, shadows, and drawing detail. `High` keeps quality-preserving optimizations active, while lower tiers cut heavier visuals for crowded scenes without lowering object limits; simulation cadence remains quality-preserving.
-- **Mobile mode controls**: auto, camera, and tool modes for touch screens.
-- **Right panel tabs**: jump between main game, selected tarinai data, tools, colony, events, and family tree.
-- **Tarinai data panel**: appears when a tarinai is selected. Shows health, needs, personality, active effects, relations, inventory, behavior reason, and records.
-- **Tool palette**: grouped buttons for operation, food, medicine, habitat, physics, and danger tools.
-- **Colony panel**: shows population, deaths, colony mood, ground type, and charts for population, needs, and objects.
-- **Event panel**: logs births, deaths, accidents, fights, food events, weather, grass, and relation changes. Optional push-style notifications can be enabled by event type.
-- **Family tree**: shows parent-child generations, inherited stats, personality tags, family links, and archived lineage records.
-- **Ecology dialog**: in-game guide explaining tarinai needs, stress, food, sleep, personality, family, disease, accidents, tools, and colony reading.
-- **Sound menu**: master sound plus voice, notification, and operation categories.
-
-## Tool Categories
-
-### Operation
-
-- **解除 / clear**: cancels the selected tool and returns to normal observe mode.
-- **たりないデータ / observe**: select a tarinai or inspect compatible objects such as signboards.
-- **追加 / new**: calls a new tarinai into the field.
-- **つつく / poke**: pokes tarinai and can push balls.
-- **つまむ / pinch**: drags tarinai and some placed objects.
-- **コピー / copy**: copies a placed item and pastes a new instance with its settings.
-- **戻す / undo** and **進む / redo**: reverse or reapply placement/edit actions.
-- **削除 / delete**: removes placed tools, objects, and stains.
-- **範囲削除 / area delete**: removes many nearby objects or stains around the cursor.
-- **洗浄 / water hose**: drag to clean dirt, zunchi, corpses, and stains.
-
-### Food
-
-- **ずんだ餅 / sweet**: favored food; can cure some illness.
-- **へこ餅 / love mochi**: makes reproduction behavior more likely for a while.
-- **けんか餅 / fight mochi**: makes fighting behavior more likely for a while.
-- **草 / grass**: food and comfort source; can grow, be used for simple beds, and sometimes dampen nearby fire instead of immediately catching.
-- **水滴 / water**: drinkable water, also appears during rain and can be set in duplicators.
-- **ずんだ汁 / zunda juice**: clears item effects from a tarinai.
-- **複製機 / duplicator**: can be loaded with compatible food, medicine, grass, water, pushpins, or oshibyo and then produces or applies that content.
+Sweet food helps satisfy hunger and can support recovery in some cases. Love mochi makes reproduction behavior more likely for a while, while fight mochi pushes the colony toward conflict. Zunda juice clears item effects, making it useful after medicine or special consumables have changed a tarinai too much.
-### Medicine
+Medicine changes the simulation more aggressively than normal food. Sleep drugs push tarinai toward sleep or sleep disease, laxatives create repeated zunchi and reduce food benefit, protein makes a stronger permanent mode with lower disease risk, and niteropu makes a weaker permanent mode with higher disease risk. Ammo creates temporary speed and impact danger. Mystery drugs choose from eligible effects unpredictably. Mercury changes lifespan, while giant and dwarf drugs reshape size, movement, power, and ongoing damage risk.
-- **ねむり薬 / sleep drug**: causes sleepiness or sleep disease; damage can cure the disease.
-- **下剤 / laxative**: causes repeated zunchi and reduces food benefit.
-- **プロテイン / protein**: permanent power mode; more damage dealt, lower disease chance.
-- **ンイテロプ / niteropu**: permanent weak mode; less damage dealt, higher disease chance.
-- **弾薬 / ammo**: temporary high-speed movement and strong collision impact.
-- **怪しい薬 / mystery drug**: randomly applies an eligible item effect or disease.
-- **水銀 / mercury**: increases lifespan multiplier on each use.
-- **巨大薬 / giant drug**: greatly increases size, attack, and speed, but deals ongoing damage.
-- **矮小薬 / dwarf drug**: greatly decreases size, attack, and speed, and also deals ongoing damage.
+## Habitat And Cleanup
-### Habitat
+Zunchi is waste, but it is also part of the ecology. It affects hygiene and disease risk and can fertilize grass. Beds and nest boxes give tarinai safer or better sleep options, reduce stress around resting areas, and help recovery. Balls give tarinai something to play with and can also become part of physics experiments.
-- **ずんち / zunchi**: tarinai waste; affects disease and can fertilize grass.
-- **ロボ掃除機 / robot_cleaner**: moves through the field at fixed normal speed 60, or 120 in high-speed mode. It checks the nearest matching cleanup target every 2 seconds, keeps pursuing it between scans, and cleans every matching object it touches. The 死骸 target also covers blood traces; clicking the item opens its target settings.
-- **干草寝床 / bed**: placeable sleeping spot; helps HP recovery.
-- **巣箱 / nest box**: reduces stress nearby; improves sleep quality when used.
-- **ボール / ball**: toy that tarinai can play with and the player can poke.
-- **看板 / signboard**: editable text object; inspect with observe after placing.
+Robot cleaners are mobile cleanup items. They scan for selected target types, pursue matching waste or corpses, and remove them on contact. The water hose gives the player direct cleanup control by washing dirt, zunchi, corpses, stains, and similar field problems. Signboards are editable objects that can be inspected after placement, useful for labeling areas or experiments.
-### Physics
-
-- **ヒモ / rope**: connect two targets with a flexible, inextensible link that may go slack but does not lengthen.
-- **棒 / rod**: connect two targets with a rigid link.
-- **柵 / fence**: blocks tarinai movement; Q/E rotate by 45 degrees, Shift+Q/E by 5 degrees.
-- **バウンス柵 / bounce fence**: high-restitution fence that strongly launches things that hit it.
-- **ゲート柵 / gate fence**: click to open or close.
-- **回転体 / rotator**: custom mechanical shape that rotates; click to edit shape and speed.
-- **毒ブロック / poison block**: custom hazard block; touching it damages skin; click to edit shape.
-- **往復体 / reciprocator**: custom mechanical shape that moves back and forth; click to edit shape and speed.
+## Physics And Machines
-### Danger
+Physics items turn the field into a mechanical sandbox. Ropes connect two targets with a flexible link that can go slack but should not stretch freely. Rods create a rigid connection. Springs connect targets with elastic motion, allowing linked objects to pull back and oscillate.
-- **石 / stone**: heavy object; dangerous when dropped or moved into others.
-- **げんこつ / genkotsu**: large fist impact on the ground.
-- **爆竹 / firecracker**: creates an explosion; flame variants scatter fire while respecting active-fire limits.
-- **画鋲 / pushpin**: rolls when dropped; can lodge in tarinai and cause panic plus continuous damage.
-- **おしり鋲 / oshibyo**: blocks zunchi when lodged; pulling it out releases stored zunchi.
-- **射撃 / shoot**: attacks with a gun effect.
-- **アリの巣 / ant nest**: spawns ants that search for tarinai and carry them to the nest.
-
-## Tarinai Needs And Stats
-
-Tarinai behavior is shaped by health, energy, needs, stress, personality, family, disease, and nearby objects.
-
-Key needs include:
-
-- **Food / hunger**: hungry tarinai seek grass, sweets, food, water-related options, or other edible targets.
-- **Sleep**: sleepy tarinai seek beds, nest boxes, grass beds, or eventually sleep anywhere.
-- **Safety**: danger, pain, hazards, ants, fights, and panic increase defensive behavior.
-- **Social / relations**: tarinai can seek friends, follow parents, avoid enemies, fight rivals, or approach mates.
-- **Fulfillment / comfort**: toys, ground, sun, shelter, and environment affect mood and stress.
-- **Health**: damage, disease, stress, and recovery decide whether tarinai live, weaken, or die.
-
-Stress changes with fear, crowding, dirt, disease, fights, ground type, and environment. High stress makes fleeing, intimidation, sickness, and poor behavior more likely.
-
-Personality and genetics affect tendencies such as aggression, openness, sociability, nervousness, body size, lifespan, damage, and speed. Some traits are inherited by children and shown in the family tree.
-
-## Movement And Behavior
-
-Tarinai are autonomous. Their movement is usually a result of current state, target choice, and local environment.
-
-Common behavior states:
-
-- **Idle / wander**: waits, watches, or moves lightly around the field.
-- **Seek food**: moves toward edible items when hungry or affected by disease.
-- **Eat / drink**: consumes food, grass, water, medicine, or special items.
-- **Seek bed / sleep**: finds a bed, nest box, grass bed, or sleeps in place.
-- **Play ball**: approaches and interacts with balls.
-- **Seek material / build**: gathers material and creates simple structures such as grass beds.
-- **Seek friend / follow parent**: moves toward social targets or parents.
-- **Fight / seek enemy**: approaches and attacks rivals, especially under fight effects or aggression.
-- **Panic / flee**: runs from danger, pain, destroyed owned objects, hazards, or fear triggers.
-- **Intimidate**: threatens another tarinai or ants instead of directly fleeing or attacking.
-- **Birth ritual**: pre-birth/reproduction interaction with a partner.
-- **Sunbath**: rests in good sun to reduce stress.
-- **Disease states**: zunchi sickness, sleep disease, explosion disease, and injury sickness can override normal priorities.
-- **Frozen**: outside-field preservation state.
-
-Tarinai can be pushed by physics objects, carried or affected by ants, dragged by the player, attached with ropes/rods, blocked by fences, and damaged by hazards. Some objects become owned or emotionally important, so losing them can trigger panic.
-
-## Environment
+Fences block movement and can be rotated for layout control. Gates can be opened or closed after placement. One-way fences allow passage from one direction while blocking the opposite direction. Bounce fences launch colliding targets more strongly than normal fences, which makes them useful for traps or kinetic experiments.
-- **Field types**: park, garden, and cage provide different field sizes.
-- **Ground types**: sand, dirt, concrete, blanket, foot massage, ice, and laboratory-style ground affect stress, grass, and movement.
-- **Weather**: rain and other weather influence water, mood, and environment effects.
-- **Grass ecology**: grass grows, feeds tarinai, comforts them, can become bed material, and has a small chance to reduce fire before burning.
-- **Dirt and corpses**: zunchi, splats, corpses, and ant corpses affect hygiene, disease, and observation results.
-- **Day/light conditions**: lighting and sun influence rendering and sunbath behavior.
-- **Season temperature**: automatic summer climate is roughly 5℃ hotter than the previous baseline; manual temperature control remains exact to the slider value.
+Rotators are editable mechanical shapes that spin and push nearby targets. Their shape and speed can create conveyors, crushers, traps, or chaotic collision fields. Reciprocators move back and forth along a line, so they can shove objects repeatedly. Poison blocks are custom hazard shapes that damage on contact.
-## Crowded Scene Performance
+Wires and insulated wires carry signals between compatible items. Normal energized wire can hurt living targets on contact, while insulated wire is safer for incidental contact. Detectors can sense targets or timing conditions and send signals, allowing simple automation.
-Crowded fields are an intended sandbox outcome. Performance work should prefer cached, typed, spatial, dirty, or incremental lookups over lowering object limits. High display quality should preserve visible fidelity and simulation rules while avoiding repeated hidden scans in render, tooltip, family tree, collision, effect, fire, and maintenance paths.
+## Danger Tools
-## Disease And Recovery
+Danger tools create immediate or ongoing pressure. Stones are heavy objects that become dangerous when dropped, thrown, or moved by machines. Genkotsu creates a large impact on the ground. Firecrackers cause explosions, with flame variants adding fire while respecting active-fire limits.
-Diseases and item effects can strongly redirect behavior.
+Pushpins can roll, lodge in tarinai, cause panic, and deal continuous damage. Oshibyo blocks zunchi while lodged and releases stored zunchi when removed. Shooting is a direct attack tool that can hit tarinai, ants, items, pins, balloons, or placed objects depending on the target under the aim point.
-- **Zunchi disease**: changes food/water priorities and makes the tarinai feel unwell.
-- **Sleep disease**: causes sleep-focused behavior.
-- **Explosion disease**: creates dangerous explosive outcomes and water-seeking pressure.
-- **Fight disease / injury sickness**: causes unstable movement or fighting-related behavior.
-- **Item effects**: mochi, medicine, size drugs, mercury, ammo, and power modes create temporary or permanent modifiers.
+Ant nests introduce an enemy system. Ants search for tarinai, carry captured targets back to the nest, and can be shot or killed. Worker ants and queen ants have separate HP values, and ant counts are shown through colony statistics.
-Recovery depends on the disease or effect. Water, rest, food, medicine, zunda juice, damage, environment cleanup, or time can all matter depending on the case.
+## Operation Tools
-## Family, Records, And Colony Observation
+The observe tool selects tarinai or compatible objects and opens their data. The add tool introduces new tarinai. Poke applies a small interaction or push, while pinch lets the player drag tarinai and some objects. Copy duplicates placed items with their settings so setups can be repeated quickly. The operation palette also provides a dedicated snap control shared by placement and pinch movement; it cycles through Free, 20px, 40px, and 60px.
-- Tarinai can form parent-child records and partner links.
-- Children inherit parts of their parents' tendencies and are grouped by generation.
-- Family tree nodes show portraits, inherited stats, personality tags, and relationship structure.
-- Event logs record important simulation events such as birth, death, accidents, fights, weather, grass, and relation changes.
-- Colony charts summarize population, needs, and object counts over time.
-- Selected tarinai records help explain why an individual is acting a certain way.
+Undo and redo reverse or reapply placement and edit actions. Delete removes a specific object or stain, while area delete removes many nearby targets at once. These tools let the player repair a field after experiments without resetting the whole colony.
-## Save, Reset, And Persistence
+## Grounds And Environment
-- The app supports manual save and restore through browser storage.
-- Current saves are allowed to use compact/hash-style text. Old save text does not need to remain loadable when the save format is intentionally shortened or replaced, so load failure is acceptable after intentional save-format changes as long as the current save/load path stays internally consistent.
-- Reset opens field selection and starts a new field state.
-- Service worker caching allows the static app shell to remain available after load, depending on browser support.
+Fields define the playable space and boundary style. Grounds change the character of the colony: sand, dirt, concrete, blanket, foot massage, ice, and laboratory-style surfaces can affect movement, stress, comfort, and grass behavior. Ground choice matters because tarinai react not only to objects, but also to the surface they live on.
-## v39.16.25 additions
+Weather changes the field over time. Rain can create water and affect comfort, shade can protect from weather or sun, and temperature changes how tarinai experience the environment. Grass, zunchi, splats, corpses, ant corpses, rain drops, and stains all feed into hygiene, disease, cleanup, rendering, and observation.
-- 雨天時は未遮蔽のたりないの体感気温が3.5℃低下し、晴天時は3.5℃上昇する。
-- 雨よけをパラソルへ変更。範囲内では雨と日差しを防ぎ、天候による体感気温補正を無効化する。
-- パラソル内では日光浴を開始・継続しない。
-- ペットフード、救急薬、鎮静薬の追加内容は維持する。
+## Colony Statistics
+The colony panel summarizes population, sick tarinai, juveniles, elders, deaths, mood, temperature, population limits, and object limits. Graph tabs show population, needs, and environment. Total ants are included in the environment statistics so active ant pressure can be read alongside other field signals.
-## v39.16.28 additions
+The event panel records important simulation events such as birth, death, fights, accidents, weather, grass changes, and relation changes. The family tree shows parent-child structure, generations, inherited stats, personality tags, and family links.
-- トイレの砂を従来より大きな角丸矩形へ変更し、干草寝床と同じ背面レイヤーで描画する。
-- トイレの砂の外観とずんち吸収判定を共通の矩形フットプリントから生成し、見た目と有効範囲を一致させる。
-- パラソルをやや大型化し、影の中心を支柱の根元へ合わせる。
-- パラソルの影の楕円を、そのまま雨・日差し・天候由来の体感気温補正を防ぐ範囲として使用する。
-- 晴天の暑さ、または雨天の寒さを避ける際、パラソルの影の内側を気温調整行動の候補にする。
-- 扇風機の体感気温低下が風の描画・物体押し出しより短い範囲で途切れていた問題を修正し、320pxの風領域全体で最大5℃の冷却を減衰適用する。
-## v39.16.29 additions
+## Save, Display, And Performance
-- Parasol placement previews use the same ellipse and center as the rendered shade and weather-protection footprint.
-- Toilet sand absorbs ambient rain-water drops before they become ground water items.
-- Happy preset adds parasols and water balloons, and places one toilet-sand item at every duplicator position.
-- Athletic preset adds water balloons and replaces complex rotators with single-bar rotators positioned using their full rotation envelope so they do not strike fences.
-- War preset adds fans with active rotating head-swing settings.
+Manual save and restore use browser storage. Reset returns to field selection and starts a new state. Service worker caching can keep the static app shell available after the first successful load when the browser supports it.
+Display settings control effects, shadows, drawing detail, and quality choices for crowded scenes. Crowded fields are an intended part of the sandbox, so the game uses spatial lookup, scheduling, cached drawing, and incremental maintenance systems to keep large colonies and many objects playable.
-## v39.16.30 additions
-- 戦争プリセットに少数のバウンス柵を追加する。
-- 通常の柵は、ボールなどに対する反発を控えめにする。
-- 巣箱の外で眠っているたりないも、ボールや水風船と物理衝突する。
-- 巣箱ツールチップを巣箱へやや近づける。
-- ボール、画鋲、ずんちなどの移動可能アイテム同士は、同じ設置位置へ重ねて配置できる。
-- 削除ツールの対象探索で発生していた例外を修正する。
-
-## v39.16.31 additions
-
-- 一触即発プリセット、ツールチップ距離の統一、物理カテゴリ専用アイコンを追加。
+## Achievement system (v39.16.56)
+- Fifty-one achievements track reproduction, natural status changes, generation runs, rapid population events, special deaths, colony conditions, individual care, autonomous ecology, history usage, cleanup, rapid deletion, temperature transitions, weather shelter, medicine-category completion, combat reversals, and ant-related play styles.
+- Locked rows reveal only the configured condition allowlist; all others show `???`. There are no achievement tooltips. 大いなる母, めっちゃたりない観察, and 敵の敵は味方 include local progress percentages, and unlocked achievements are sorted first inside each category.
+- Each achievement row behaves as a horizontal bar chart. Its background fill visualizes the optional global completion percentage from 0–100%, and the global achiever count is displayed beside it.
+- `物でいっぱい` requires 100 cumulative player placements across field resets. `害虫駆除` requires 100 cumulative ant kills across field resets.
+- `餌やりじいさん` requires 33 direct feedings in one field; resetting the field resets this progress.
+- `機械化産業` replaces the former detector-automation and link-craftsman achievements and unlocks only when both requirements are met.
+- v39.16.56 adds 混沌ヲ希求スル者 and 潔癖症, raises 清掃業者 to 200, starts ミニマリスト from day 6, excludes chain/crown from おくすり手帳全埋め, and keeps shared-rate bars visible after unlock.
+- v39.16.54 added 真・たりない観察, 悠久の歴史, 五体満足, and 零体満足. v39.16.55 added 町のお医者さん for 50 effective first-aid recoveries and 割とふわふわ for flinging a plushie with つつく. ベビーブーム now requires 30 births in one minute; 管理された楽園 starts tracking from play day 6 and requires one uninterrupted happy in-game day.
+- Local unlocks and persistent totals live in browser storage. Field-scoped direct-feeding, cleaner, self-sufficiency, enemy-ant, generation, observation, safety, and per-entity/item progress follow current-format saves where applicable. Nest-box and pipe occupants block rain/weather effects and count as sheltered for 雨宿り完了.
+- Achievement evaluation is primarily event-driven; periodic world scans skip completed or irrelevant conditions. The server writes compact v2 achievement aggregate JSON while continuing to read the earlier verbose state format.
diff --git a/README.md b/README.md
index 4013a82..e440d79 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,16 @@ AI-first entrypoint for a static browser simulation/game. Keep this file small;
## Runtime Shape
-- No bundler; `index.html` loads ordered global-IIFE scripts with `?v=39.16.31`.
+- v39.16.37 changes: biological and environmental conditions are unified into the detector; the standalone sensor and relay are removed; temperature and limits are folded; ecology, sound, display settings, and credits are in the header menu; reset remains outside in red; tarinai-data navigation preserves the active tool; mobile tool descriptions appear at the top for five seconds. Snap mode remains Free/20px/40px/60px, top-edge placement is fixed, and preset descriptions end with punctuation.
+- v39.16.38 changes: detector numeric controls use value-labelled sliders like the time control; colony temperature and limits share one Colony settings fold; display settings use an inline menu matching sound settings; HP/stress meter colors are fixed; the snap control follows Clear with a faint grid icon; area-delete has a new icon; the Ant chart series is renamed and hidden initially.
+- v39.16.55 changes: expanded the achievement set to 49 with 町のお医者さん and 割とふわふわ; changed ベビーブーム to 30 births per minute and 管理された楽園 to one happy in-game day from play day 6; restored shared poke targeting/highlights and plushie fling; corrected manual rotator/reciprocator push direction; tightened tool/limit UI; reduced periodic achievement scans; and introduced compact achievement aggregate storage.
+- v39.16.52 changes: reorganized achievements into collapsible categories, removed achievement tooltips, and moved confirmation flows into the game UI. Previous v39.16.51 changes: expanded the achievement set to 43 with sixteen play-style and special-action conditions; added persistent Undo/Redo and quick-delete counters, field-local cleaner/automation-free ecology progress, individual care/temperature/fight tracking, complete medicine-category collection, protected rain shelter in nest boxes and pipes, and stricter ant-kill attribution.
+- v39.16.49 changes: added additive full-state synchronization so shared achievement aggregates can be rebuilt as players return after server data loss.
+- v39.16.48 changes: corrected the zunchi-slave threshold text to 30%; repaired wire placement counting, danger-kill attribution, partial legacy link migration, UUID validation, and debug-only achievement isolation.
+- v39.16.47 changes: Two achievements are added (大いなる母 and 喧嘩両成敗); five names and five revealed descriptions are revised; `?debug=1` exposes an all-achievements button.
+- v39.16.46 changes: achievement names and presentation are revised; conditions are revealed only after unlock; shared completion rates render as horizontal background bars; automation and link coverage merge into 機械化産業; placement and ant-kill targets are persistent totals of 100; direct feeding resets with the field.
+- v39.16.45 changes: seven play-style achievements add passive observation, cumulative placement, detector automation, link-tool coverage, safe large-colony operation, ant population, and ant elimination measurements. World-scoped timers and placement counts persist in current-format saves.
+- No bundler; `index.html` loads ordered global-IIFE scripts with `?v=39.16.56`.
- v39.16.31 changes: all in-world tooltips use the closer offset; Hair Trigger adds a central bounce-fence ball enclosure with Tarinai and pet-food duplicators outside; physics tools use distinct icons.
- v39.16.29 changes: parasol placement preview now uses the exact shade center and footprint; toilet sand absorbs rain-created water drops. Happy adds parasols, water balloons, and toilet sand beneath each duplicator; Athletic uses fence-safe single-bar rotators and water balloons; War adds rotating fans.
- v39.16.19 changes: food and medicine can be given directly by placement or pinched drop, with a dedicated overlay, food heart bursts, and stronger personality effects; unused collision reasons and duplicate constraint projection dirty checks were removed.
@@ -59,3 +68,23 @@ AI-first entrypoint for a static browser simulation/game. Keep this file small;
### v39.16.16
Colony birth/death chart lines, reverse rotators, champion-trait inheritance, reduced champion-pair mating, and directional one-way-fence routing.
+
+- v39.16.39 changes: restored the compact sticky operation palette, added exact-match right-panel search, reorganized colony settings, and refreshed tool icons.
+
+
+## Achievement system (v39.16.56)
+- Fifty-one local achievements cover biological events, special deaths, colony conditions, individual care, passive ecology, Undo/Redo behavior, rapid deletion, cleaner use, temperature transitions, medicine-category completion, weather shelter, combat reversals, and ant-related play styles.
+- Locked entries show the configured pre-unlock disclosure directly in the row; all other locked entries show `???`. 大いなる母, めっちゃたりない観察, and 敵の敵は味方 show local progress percentages. Unlocked entries appear first within each category.
+- Each entry is a horizontal completion-rate bar whose background fill reflects the optional shared global unlock percentage; the shared achiever count appears beside the percentage.
+- v39.16.54 added 真・たりない観察, 悠久の歴史, 五体満足, and 零体満足. v39.16.55 added 町のお医者さん for 50 effective first-aid recoveries and 割とふわふわ for flinging a plushie with つつく; both are event-driven and 町のお医者さん is disclosed before unlock. v39.16.56 adds two more achievements, persistent fight progress, stricter robot-only evaluation, revised thresholds, and unlocked-card rate bars.
+- Persistent totals include 物でいっぱい, 害虫駆除, 大いなる母(3333体), 無計画都市, やっぱなし, and やっぱあり. Field-local progress includes 餌やりじいさん, 清掃業者, 自給自足, and 敵の敵は味方; applicable field-local metadata follows current-format saves.
+- `自動化の第一歩` and `リンク職人` are retired and replaced by `機械化産業`, which requires both detector-driven activation and rope/rod/spring coverage. Existing players with both retired achievements migrate automatically.
+- Optional same-origin PHP API aggregates anonymous player counts and global unlock percentages. Raw aggregate data is stored under `.achievement_data/state.json` in compact v2 arrays, with transparent reading/migration of the prior verbose format, and is protected by Apache rules.
+- On startup, shared-stat refresh, achievement-screen opening, and every five minutes, the client sends its complete non-debug unlock set with original local timestamps. Ordinary unlock bursts send only unsynchronized records after a short debounce. The server adds only missing records, so a lost `state.json` is rebuilt incrementally as players return without overwriting surviving records.
+
+### Shared achievement percentage deployment
+- Upload `achievement_api.php` and the hidden `.achievement_data` directory with the game.
+- PHP must be enabled, and `.achievement_data` must be writable by the PHP process.
+- On a static-only host, local unlocks still work; the global percentage is shown as unavailable.
+- Set `window.TARINAI_ACHIEVEMENT_API` before `achievements.js` to use a different same-origin endpoint.
+- The current release tracks fifty-one achievement IDs, supports per-player reset, and accepts additive bulk synchronization for automatic recovery after aggregate-data loss. A separate backup remains necessary to recover players who never return.
\ No newline at end of file
diff --git a/achievement_api.php b/achievement_api.php
new file mode 100644
index 0000000..94c1d8f
--- /dev/null
+++ b/achievement_api.php
@@ -0,0 +1,285 @@
+ [], 'unlocks' => []];
+}
+
+function normalize_state(mixed $value): array {
+ if (!is_array($value)) return empty_state();
+ $players = [];
+ $unlocks = [];
+
+ if (($value['v'] ?? null) === 2 && isset($value['p'], $value['u']) && is_array($value['p']) && is_array($value['u'])) {
+ $playerIds = [];
+ foreach ($value['p'] as $row) {
+ if (!is_array($row) || !isset($row[0]) || !is_string($row[0]) || !valid_player_id($row[0])) continue;
+ $id = $row[0];
+ $playerIds[] = $id;
+ $players[$id] = [
+ 'firstSeen' => max(1, (int)($row[1] ?? 1)),
+ 'lastSeen' => max(1, (int)($row[2] ?? ($row[1] ?? 1))),
+ 'gameVersion' => substr((string)($row[3] ?? ''), 0, 32),
+ ];
+ }
+ foreach ($value['u'] as $achievementId => $flat) {
+ if (!is_string($achievementId) || !is_array($flat)) continue;
+ $records = [];
+ $count = count($flat);
+ for ($i = 0; $i + 1 < $count; $i += 2) {
+ $playerIndex = (int)$flat[$i];
+ if (!isset($playerIds[$playerIndex])) continue;
+ $records[$playerIds[$playerIndex]] = max(1, (int)$flat[$i + 1]);
+ }
+ if ($records) $unlocks[$achievementId] = $records;
+ }
+ } else {
+ $players = isset($value['players']) && is_array($value['players']) ? $value['players'] : [];
+ $unlocks = isset($value['unlocks']) && is_array($value['unlocks']) ? $value['unlocks'] : [];
+ }
+
+ $legacySignal = isset($unlocks['signal_automation_first']) && is_array($unlocks['signal_automation_first'])
+ ? $unlocks['signal_automation_first'] : [];
+ $legacyLinks = isset($unlocks['link_craftsman']) && is_array($unlocks['link_craftsman'])
+ ? $unlocks['link_craftsman'] : [];
+ if ($legacySignal && $legacyLinks) {
+ if (!isset($unlocks['mechanized_industry']) || !is_array($unlocks['mechanized_industry'])) {
+ $unlocks['mechanized_industry'] = [];
+ }
+ foreach ($legacySignal as $playerId => $signalTime) {
+ if (!isset($legacyLinks[$playerId])) continue;
+ $unlocks['mechanized_industry'][$playerId] = max((int)$signalTime, (int)$legacyLinks[$playerId]);
+ }
+ }
+ unset($unlocks['signal_automation_first'], $unlocks['link_craftsman']);
+
+ return ['players' => $players, 'unlocks' => $unlocks];
+}
+
+function compact_state(array $state): array {
+ $playerIds = array_keys($state['players']);
+ sort($playerIds, SORT_STRING);
+ $playerIndex = [];
+ $players = [];
+ foreach ($playerIds as $index => $id) {
+ $playerIndex[$id] = $index;
+ $player = is_array($state['players'][$id] ?? null) ? $state['players'][$id] : [];
+ $players[] = [
+ $id,
+ max(1, (int)($player['firstSeen'] ?? 1)),
+ max(1, (int)($player['lastSeen'] ?? ($player['firstSeen'] ?? 1))),
+ substr((string)($player['gameVersion'] ?? ''), 0, 32),
+ ];
+ }
+ $unlocks = [];
+ foreach ($state['unlocks'] as $achievementId => $records) {
+ if (!is_string($achievementId) || !is_array($records) || !$records) continue;
+ $flat = [];
+ foreach ($records as $id => $timestamp) {
+ if (!isset($playerIndex[$id])) continue;
+ $flat[] = $playerIndex[$id];
+ $flat[] = max(1, (int)$timestamp);
+ }
+ if ($flat) $unlocks[$achievementId] = $flat;
+ }
+ ksort($unlocks, SORT_STRING);
+ return ['v' => 2, 'p' => $players, 'u' => $unlocks];
+}
+
+function build_summary(array $state): array {
+ $total = count($state['players']);
+ $achievements = [];
+ foreach (ACHIEVEMENT_IDS as $id) {
+ $unlocked = isset($state['unlocks'][$id]) && is_array($state['unlocks'][$id])
+ ? count($state['unlocks'][$id])
+ : 0;
+ $percentage = $total > 0 ? round(($unlocked / $total) * 100, 1) : 0.0;
+ $achievements[$id] = [
+ 'unlockedPlayers' => $unlocked,
+ 'percentage' => $percentage,
+ ];
+ }
+ return [
+ 'ok' => true,
+ 'totalPlayers' => $total,
+ 'achievements' => $achievements,
+ ];
+}
+
+$method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');
+$payload = [];
+if ($method === 'POST') {
+ $raw = file_get_contents('php://input');
+ $decoded = json_decode(is_string($raw) ? $raw : '', true);
+ if (!is_array($decoded)) respond(['ok' => false, 'error' => 'invalid_json'], 400);
+ $payload = $decoded;
+}
+
+$action = $method === 'POST'
+ ? (string)($payload['action'] ?? '')
+ : (string)($_GET['action'] ?? 'summary');
+$playerId = $method === 'POST'
+ ? (string)($payload['playerId'] ?? '')
+ : (string)($_GET['playerId'] ?? '');
+
+if (!in_array($action, ['session', 'unlock', 'sync', 'reset', 'summary'], true)) {
+ respond(['ok' => false, 'error' => 'invalid_action'], 400);
+}
+if (!valid_player_id($playerId)) {
+ respond(['ok' => false, 'error' => 'invalid_player'], 400);
+}
+$syncUnlocks = [];
+if ($action === 'unlock') {
+ $achievementId = (string)($payload['achievementId'] ?? '');
+ if (!in_array($achievementId, ACHIEVEMENT_IDS, true)) {
+ respond(['ok' => false, 'error' => 'invalid_achievement'], 400);
+ }
+} elseif ($action === 'sync') {
+ $submittedUnlocks = $payload['unlocked'] ?? null;
+ if (!is_array($submittedUnlocks) || count($submittedUnlocks) > count(ACHIEVEMENT_IDS)) {
+ respond(['ok' => false, 'error' => 'invalid_unlocks'], 400);
+ }
+ foreach ($submittedUnlocks as $achievementId => $timestamp) {
+ if (!is_string($achievementId) || !in_array($achievementId, ACHIEVEMENT_IDS, true)) {
+ respond(['ok' => false, 'error' => 'invalid_achievement'], 400);
+ }
+ if (!is_int($timestamp) && !is_float($timestamp) && !is_string($timestamp)) {
+ respond(['ok' => false, 'error' => 'invalid_timestamp'], 400);
+ }
+ $numericTimestamp = filter_var($timestamp, FILTER_VALIDATE_FLOAT);
+ if ($numericTimestamp === false || $numericTimestamp <= 0) {
+ respond(['ok' => false, 'error' => 'invalid_timestamp'], 400);
+ }
+ $syncUnlocks[$achievementId] = (float)$numericTimestamp;
+ }
+}
+
+$dataDir = __DIR__ . DIRECTORY_SEPARATOR . DATA_DIR_NAME;
+if (!is_dir($dataDir) && !mkdir($dataDir, 0775, true) && !is_dir($dataDir)) {
+ respond(['ok' => false, 'error' => 'storage_unavailable'], 503);
+}
+$dataPath = $dataDir . DIRECTORY_SEPARATOR . DATA_FILE_NAME;
+$handle = fopen($dataPath, 'c+');
+if ($handle === false || !flock($handle, LOCK_EX)) {
+ if (is_resource($handle)) fclose($handle);
+ respond(['ok' => false, 'error' => 'storage_locked'], 503);
+}
+
+try {
+ rewind($handle);
+ $contents = stream_get_contents($handle);
+ $state = normalize_state(json_decode(is_string($contents) ? $contents : '', true));
+ $now = time();
+ $player = isset($state['players'][$playerId]) && is_array($state['players'][$playerId])
+ ? $state['players'][$playerId]
+ : ['firstSeen' => $now];
+ $player['lastSeen'] = $now;
+ $player['gameVersion'] = substr((string)($payload['gameVersion'] ?? ''), 0, 32);
+ $state['players'][$playerId] = $player;
+
+ if ($action === 'unlock') {
+ $achievementId = (string)$payload['achievementId'];
+ if (!isset($state['unlocks'][$achievementId]) || !is_array($state['unlocks'][$achievementId])) {
+ $state['unlocks'][$achievementId] = [];
+ }
+ if (!isset($state['unlocks'][$achievementId][$playerId])) {
+ $state['unlocks'][$achievementId][$playerId] = $now;
+ }
+ } elseif ($action === 'sync') {
+ foreach ($syncUnlocks as $achievementId => $submittedTimestamp) {
+ if (!isset($state['unlocks'][$achievementId]) || !is_array($state['unlocks'][$achievementId])) {
+ $state['unlocks'][$achievementId] = [];
+ }
+ if (isset($state['unlocks'][$achievementId][$playerId])) continue;
+ $timestampSeconds = $submittedTimestamp >= 100000000000
+ ? (int)floor($submittedTimestamp / 1000)
+ : (int)floor($submittedTimestamp);
+ $state['unlocks'][$achievementId][$playerId] = max(1, min($now, $timestampSeconds));
+ }
+ } elseif ($action === 'reset') {
+ foreach ($state['unlocks'] as $achievementId => $records) {
+ if (is_array($records)) unset($state['unlocks'][$achievementId][$playerId]);
+ }
+ }
+
+ rewind($handle);
+ ftruncate($handle, 0);
+ fwrite($handle, json_encode(compact_state($state), JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE));
+ fflush($handle);
+ $summary = build_summary($state);
+} finally {
+ flock($handle, LOCK_UN);
+ fclose($handle);
+}
+
+respond($summary);
diff --git a/app_manifest.json b/app_manifest.json
index e04734f..39a07a3 100644
--- a/app_manifest.json
+++ b/app_manifest.json
@@ -1,5 +1,5 @@
{
- "version": "39.16.31",
+ "version": "39.16.56",
"css": [
"css/base.css",
"css/layout.css",
@@ -145,6 +145,8 @@
"js/command_dispatcher.js",
"js/text_catalog.js",
"js/ui.js",
+ "js/game_dialogs.js",
+ "js/achievements.js",
"js/ui_log.js",
"js/ui_helpers.js",
"js/ui_selected.js",
diff --git a/css/base.css b/css/base.css
index 8b88845..cb70d5f 100644
--- a/css/base.css
+++ b/css/base.css
@@ -325,13 +325,13 @@ h1 {
--tool-watermark-size: 0px;
border-radius: 12px;
display: grid;
- grid-template-columns: 29px minmax(0,1fr);
+ grid-template-columns: 31px minmax(0,1fr);
align-items: center;
justify-items: stretch;
- gap: 3px;
- padding: 4px 4px 4px 2px;
+ gap: 2px;
+ padding: 3px 3px 3px 2px;
min-height: 47px;
- font-size: 10.2px;
+ font-size: 11.4px;
line-height: 1.06;
letter-spacing: -0.015em;
text-align: left;
@@ -374,16 +374,16 @@ h1 {
max-width: 100%;
min-width: 0;
overflow-wrap: anywhere;
- font-size: 10.2px;
- line-height: 1.10;
+ font-size: 11.4px;
+ line-height: 1.08;
font-weight: 700;
- max-width: 5.05em;
+ max-width: 5.35em;
word-break: normal;
overflow-wrap: anywhere;
text-shadow: 0 1px 0 rgba(255,255,255,0.62);
}
-.tool.tool-has-canvas-icon { --tool-icon: none; grid-template-columns: 29px minmax(0,1fr); min-height: 47px; padding-top: 4px; }
+.tool.tool-has-canvas-icon { --tool-icon: none; grid-template-columns: 31px minmax(0,1fr); min-height: 47px; padding-top: 4px; }
.tool.tool-has-canvas-icon::before,
.tool.tool-has-canvas-icon::after { background: none !important; }
@@ -621,6 +621,68 @@ h1 {
padding: 4px 6px;
}
+
+.pressure-numeric-control[hidden] { display: none; }
+.pressure-numeric-control {
+ display: grid;
+ gap: 8px;
+ padding: 10px 10px 8px;
+ border-radius: 11px;
+ border: 1px solid rgba(84,68,48,0.13);
+ background: rgba(255,255,255,0.58);
+}
+.pressure-value-slider {
+ width: calc(100% - 14px);
+ height: 34px;
+ margin: 0 7px;
+ padding: 0;
+ appearance: none;
+ -webkit-appearance: none;
+ background: transparent;
+}
+.pressure-value-slider::-webkit-slider-runnable-track {
+ height: 6px;
+ border-radius: 999px;
+ background: rgba(67,158,218,0.78);
+ box-shadow: inset 0 1px 2px rgba(43,55,66,0.16);
+}
+.pressure-value-slider::-webkit-slider-thumb {
+ width: 20px;
+ height: 20px;
+ margin-top: -7px;
+ border-radius: 50%;
+ border: 2px solid rgba(42,121,172,0.88);
+ background: #fafdff;
+ box-shadow: 0 2px 6px rgba(42,78,104,0.24);
+ appearance: none;
+ -webkit-appearance: none;
+ cursor: grab;
+}
+.pressure-value-slider::-moz-range-track {
+ height: 6px;
+ border: 0;
+ border-radius: 999px;
+ background: rgba(67,158,218,0.78);
+ box-shadow: inset 0 1px 2px rgba(43,55,66,0.16);
+}
+.pressure-value-slider::-moz-range-thumb {
+ width: 17px;
+ height: 17px;
+ border-radius: 50%;
+ border: 2px solid rgba(42,121,172,0.88);
+ background: #fafdff;
+ box-shadow: 0 2px 6px rgba(42,78,104,0.24);
+ cursor: grab;
+}
+.pressure-value-slider:focus-visible::-webkit-slider-thumb {
+ outline: 3px solid rgba(73,157,216,0.30);
+ outline-offset: 2px;
+}
+.pressure-value-slider:focus-visible::-moz-range-thumb {
+ outline: 3px solid rgba(73,157,216,0.30);
+ outline-offset: 2px;
+}
+
.pressure-time-control[hidden] { display: none; }
.pressure-spatial-control[hidden] { display: none; }
.pressure-time-control {
diff --git a/css/components.css b/css/components.css
index 240b0f3..f7c23e1 100644
--- a/css/components.css
+++ b/css/components.css
@@ -643,129 +643,145 @@
font-size: 12px;
line-height: 1.45;
}
-.visual-settings-dialog-panel {
- width: min(560px, calc(100vw - 32px));
+/* v39.16.40: compact header menu and folded colony settings. */
+.top-menu { position: relative; display: inline-block; }
+.top-menu-btn {
+ min-width: 40px;
+ padding-inline: 11px;
+ font-size: 20px;
+ line-height: 1;
+ font-family: system-ui, sans-serif;
}
-.visual-setting-row {
+.top-menu-panel {
+ position: absolute;
+ right: 0;
+ top: calc(100% + 8px);
+ z-index: 14000;
+ width: min(250px, calc(100vw - 20px));
+ padding: 8px;
+ border: 1px solid rgba(84,68,48,.16);
+ border-radius: 16px;
+ background: rgba(255,252,246,.98);
+ box-shadow: 0 16px 42px rgba(44,34,25,.20);
display: grid;
- grid-template-columns: minmax(0, 1fr) auto;
- gap: 12px;
- align-items: center;
- padding: 12px 0;
- border-top: 1px solid rgba(115, 91, 62, 0.16);
-}
-.visual-setting-row strong,
-.visual-setting-row span {
- display: block;
-}
-.visual-setting-row strong {
- color: #4f3928;
- font-size: 15px;
-}
-.visual-setting-row span {
- margin-top: 3px;
- color: #6d5a46;
- font-size: 12px;
-}
-.visual-setting-options {
- display: inline-flex;
gap: 5px;
- padding: 4px;
- border: 1px solid rgba(115, 91, 62, 0.18);
- border-radius: 999px;
- background: rgba(255, 255, 255, 0.50);
}
-.visual-setting-options button {
- min-width: 46px;
- min-height: 32px;
- border: 1px solid transparent;
- border-radius: 999px;
+.top-menu-panel.hidden { display: none; }
+.top-menu-item {
+ width: 100%;
+ min-height: 38px;
+ border: 0;
+ border-radius: 11px;
+ padding: 8px 10px;
background: transparent;
- color: #5d4938;
- cursor: pointer;
+ color: var(--ink);
+ text-align: left;
font: inherit;
- font-weight: 800;
+ font-size: 13px;
+ font-weight: 700;
+ cursor: pointer;
}
-.visual-setting-options button.active,
-.visual-setting-options button[aria-pressed="true"] {
- border-color: rgba(127, 94, 54, 0.28);
- background: #fff4d8;
- color: #3f3023;
- box-shadow: 0 2px 8px rgba(96, 70, 40, 0.12);
-}
-.visual-setting-options button:focus-visible {
- outline: 2px solid rgba(184, 128, 54, 0.45);
- outline-offset: 2px;
-}
-@media (max-width: 640px) {
- .save-slot-row {
- grid-template-columns: 1fr;
- }
- .save-slot-actions {
- justify-content: flex-start;
- }
- .visual-setting-row {
- grid-template-columns: 1fr;
- }
- .visual-setting-options {
- width: 100%;
- justify-content: stretch;
- }
- .visual-setting-options button {
- flex: 1 1 0;
- }
+.top-menu-item:hover,
+.top-menu-item:focus-visible { background: rgba(238,245,225,.88); outline: none; }
+.top-menu-sound { display: block; width: 100%; }
+.top-menu-sound .sound-menu-btn { display: block; border: 0; box-shadow: none; }
+.top-menu-sound .sound-panel {
+ position: static;
+ width: auto;
+ min-width: 0;
+ margin: 3px 3px 5px;
+ padding: 9px 10px;
+ border-radius: 12px;
+ box-shadow: none;
+ background: rgba(245,240,229,.78);
}
-.ground-stat {
- padding: 7px 8px;
+.top-menu-visual { display: block; width: 100%; }
+.sound-panel-subtitle {
+ padding: 2px 0 1px;
+ color: var(--muted);
+ font-size: 10px;
+ font-weight: 800;
+ letter-spacing: .05em;
}
-.ground-type-btn {
- width: 100%;
- min-height: 28px;
- border: 1px solid rgba(92, 142, 74, 0.28);
- border-radius: 999px;
- background: rgba(255,255,255,0.58);
- color: inherit;
- cursor: pointer;
- font: inherit;
- box-shadow: inset 0 1px 0 rgba(255,255,255,0.55);
-}
-.ground-type-btn:hover {
- background: rgba(255,255,255,0.82);
- transform: translateY(-1px);
-}
-.ground-type-btn strong {
- display: inline-block;
- font-size: 16px;
- line-height: 1.2;
-}
-.ground-stat[data-ground-toggle] {
+.top-menu-visual .sound-panel label { min-height: 26px; }
+
+.reset-top-btn {
+ color: #8e2424 !important;
+ border-color: rgba(182,69,69,.48) !important;
+ background: linear-gradient(180deg, rgba(255,245,245,.96), rgba(255,224,224,.92)) !important;
+ font-weight: 800;
+}
+.reset-top-btn:hover { background: linear-gradient(180deg, #fff4f4, #ffd6d6) !important; }
+
+
+.colony-settings-body { display: grid; gap: 0; }
+.colony-setting-section { padding: 10px; }
+.colony-setting-section + .colony-setting-section { border-top: 1px solid rgba(118,92,61,.12); }
+.colony-setting-section-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 7px; color: var(--ink); font-size: 12px; font-weight: 800; }
+.colony-setting-section-head strong { font-size: 15px; }
+.colony-setting-section-head small { margin-left: auto; color: var(--muted); font-size: 10px; font-weight: 500; }
+
+.colony-setting-fold {
+ margin: 8px 0 4px;
+ border: 1px solid rgba(118,92,61,.16);
+ border-radius: 14px;
+ background: rgba(255,251,244,.56);
+ overflow: hidden;
+}
+.colony-setting-fold > summary {
+ min-height: 38px;
+ box-sizing: border-box;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 11px;
+ color: rgba(68,55,41,.84);
+ font-size: 12px;
+ font-weight: 700;
cursor: pointer;
+ list-style: none;
user-select: none;
}
-.ground-stat[data-ground-toggle]:focus-visible,
-.ground-type-btn:focus-visible {
- outline: 2px solid rgba(103,178,44,0.58);
- outline-offset: 2px;
+.colony-setting-fold > summary::-webkit-details-marker { display: none; }
+.colony-setting-fold > summary::before { content: "\25b8"; color: var(--muted); width: 12px; }
+.colony-setting-fold[open] > summary::before { content: "\25be"; }
+.colony-setting-fold > summary strong { margin-left: auto; font-size: 15px; }
+.colony-setting-fold > summary small { margin-left: auto; color: var(--muted); font-size: 10px; font-weight: 500; }
+.colony-setting-fold[open] > summary { border-bottom: 1px solid rgba(118,92,61,.12); }
+.colony-setting-fold .temperature-control,
+.colony-setting-fold .colony-limit-control {
+ margin: 0;
+ border: 0;
+ border-radius: 0;
+ background: transparent;
}
-/* v36: pin only the operation tool category under the quick-scroll tabs. */
+.colony-setting-fold .temperature-control { display: grid; gap: 5px; }
+.colony-setting-fold .temperature-auto { margin-left: 0; justify-self: end; }
+
+/* v39.16.40: one unified colony settings fold. */
+.colony-settings-fold .temperature-control,
+.colony-settings-fold .colony-limit-control { padding: 0; border: 0; border-radius: 0; background: transparent; }
+
+
+/* v39.16.40: restore the compact sticky operation palette from the earlier UI. */
.tool-category[data-tool-category="operate"]:not(.collapsed) {
position: sticky;
top: 58px;
z-index: 11;
overflow: hidden;
- background: linear-gradient(180deg, rgba(238, 247, 255, 0.98), rgba(221, 237, 255, 0.98));
- border: 3px solid rgba(52,112,188,0.72);
- box-shadow: 0 9px 22px rgba(58, 82, 112, 0.18), inset 0 0 0 1px rgba(255,255,255,0.86);
+ background: linear-gradient(180deg, rgba(238,247,255,.98), rgba(221,237,255,.98));
+ border: 3px solid rgba(52,112,188,.72);
+ box-shadow: 0 9px 22px rgba(58,82,112,.18), inset 0 0 0 1px rgba(255,255,255,.86);
}
.tool-category[data-tool-category="operate"] .tool-category-toggle {
- background: linear-gradient(180deg, rgba(255,255,255,0.98), rgba(231, 243, 255, 0.98));
- border-bottom-color: rgba(82,128,190,0.30);
+ background: linear-gradient(180deg, rgba(255,255,255,.98), rgba(231,243,255,.98));
+ border-bottom-color: rgba(82,128,190,.30);
}
.tool-category[data-tool-category="operate"] .tool-category-body {
- grid-template-columns: repeat(6, minmax(0, 1fr));
+ grid-template-columns: repeat(6, minmax(0,1fr));
gap: 5px;
- background: rgba(238, 247, 255, 0.98);
+ background: rgba(238,247,255,.98);
}
.tool-category[data-tool-category="operate"] .tool {
grid-template-columns: 1fr;
@@ -774,112 +790,442 @@
justify-items: center;
align-items: center;
text-align: center;
- background: rgba(255,255,255,0.96);
- border-color: rgba(82,128,190,0.28);
-}
-.tool-category[data-tool-category="operate"] .tool-label {
- display: none;
+ background: rgba(255,255,255,.96);
+ border-color: rgba(82,128,190,.28);
}
+.tool-category[data-tool-category="operate"] .tool-label { display: none; }
.tool-category[data-tool-category="operate"] .tool::before {
- left: 50%;
- top: 50%;
- transform: translate(-50%, -50%) scale(var(--tool-icon-zoom, 1));
+ left: 50%; top: 50%; transform: translate(-50%,-50%) scale(var(--tool-icon-zoom,1));
}
.tool-category[data-tool-category="operate"] .tool-sprite-preview {
- left: 50%;
- top: 50%;
- transform: translate(-50%, -50%);
+ left: 50%; top: 50%; transform: translate(-50%,-50%);
}
.tool-category[data-tool-category="operate"] .tool-shortcut {
+ position: absolute; right: 3px; bottom: 2px; min-width: 13px; height: 13px;
+ box-sizing: border-box; padding: 0 3px; border: 1px solid rgba(82,128,190,.26);
+ border-radius: 5px; background: rgba(255,255,255,.88); color: rgba(48,74,112,.86);
+ font: 700 9px/12px system-ui,sans-serif; text-align: center; pointer-events: none;
+}
+
+/* A trash can inside a selection square for range delete. */
+.tool[data-tool="area_delete"].tool-emoji-icon::before {
+ content: "";
+ box-sizing: border-box;
+ width: 30px;
+ height: 30px;
+ border: 2.5px solid #a8444d;
+ border-radius: 4px;
+ background: rgba(255,255,255,.40);
+ filter: none;
+}
+.tool[data-tool="area_delete"].tool-emoji-icon::after {
+ content: "";
+ display: block;
position: absolute;
- right: 3px;
- bottom: 2px;
- min-width: 13px;
+ left: 50%;
+ top: 52%;
+ width: 11px;
height: 13px;
box-sizing: border-box;
- padding: 0 3px;
- border: 1px solid rgba(82,128,190,0.26);
- border-radius: 5px;
- background: rgba(255,255,255,0.88);
- color: rgba(48,74,112,0.86);
- font: 700 9px/12px system-ui, sans-serif;
- text-align: center;
+ transform: translate(-50%,-50%);
+ border: 2px solid #a8444d;
+ border-top-width: 3px;
+ border-radius: 1px 1px 3px 3px;
+ background: repeating-linear-gradient(90deg, transparent 0 2px, rgba(168,68,77,.75) 2px 3px, transparent 3px 5px);
+ box-shadow: 0 -5px 0 -3px #a8444d;
+ z-index: 3;
pointer-events: none;
}
-/* Collapsed event card must shrink to its header.
- layout.css gives .log-card a min-height, which left a large blank panel when folded. */
-.log-card.collapsed { min-height: 0 !important; height: auto !important; flex: 0 0 auto !important; flex-basis: auto !important; }
-.log-card.collapsed .panel-card-body { display: none !important; }
-
-
-.temperature-control {
- margin: 10px 0 2px;
- padding: 10px 11px 11px;
- border: 1px solid rgba(118, 92, 61, 0.16);
- border-radius: 14px;
- background: rgba(255, 251, 244, 0.56);
+.tool-category[data-tool-category="operate"] .tool[data-tool="area_delete"].tool-emoji-icon::before {
+ top: 50% !important;
+ transform: translate(-50%,-50%) !important;
}
-.temperature-control-head {
+
+/* Colony settings use the colony card's own button palette. */
+.colony-card .colony-setting-fold {
+ border-color: var(--ui-theme-mid);
+ background: var(--ui-theme-soft);
+}
+.colony-card .colony-setting-fold > summary {
+ border-color: var(--ui-theme-mid);
+ background: linear-gradient(180deg, rgba(255,255,255,.78), var(--ui-theme-soft-2));
+ color: var(--ink);
+}
+.colony-card .colony-setting-fold[open] > summary { border-bottom-color: var(--ui-theme-mid); }
+.colony-card .colony-setting-section + .colony-setting-section { border-top-color: rgba(var(--ui-theme-rgb),.22); }
+.colony-stats { grid-template-columns: 1fr; }
+.ground-type-btn.colony-setting-action {
+ width: 100%;
+ min-height: 38px;
display: flex;
align-items: center;
- gap: 8px;
- font-size: 12px;
- color: rgba(68, 55, 41, 0.84);
+ justify-content: space-between;
+ gap: 10px;
+ padding: 7px 10px;
+ border: 1px solid var(--ui-theme-mid);
+ border-radius: 11px;
+ background: linear-gradient(180deg, rgba(255,255,255,.76), var(--ui-theme-soft-2));
+ color: var(--ink);
+ cursor: pointer;
+ font: inherit;
}
-.temperature-control-head strong {
- margin-left: auto;
- font-size: 15px;
+.ground-type-btn.colony-setting-action:hover { background: rgba(var(--ui-theme-rgb),.16); }
+.ground-type-btn.colony-setting-action strong { font-size: 15px; }
+.ground-type-btn.colony-setting-action span { color: var(--muted); font-size: 11px; font-weight: 700; }
+
+/* Shared panel-search primitives. */
+.panel-search-control {
+ grid-column: 1 / -1;
+ display: grid;
+ grid-template-columns: minmax(0,1fr) auto 32px;
+ align-items: center;
+ gap: 5px;
+ margin-top: 2px;
+ padding-top: 6px;
+ border-top: 1px solid rgba(82,128,190,.20);
}
-.temperature-auto {
- display: inline-flex;
+.panel-search-input {
+ width: 100%; min-width: 0; height: 32px; box-sizing: border-box;
+ padding: 6px 8px; border: 1px solid rgba(82,128,190,.34); border-radius: 9px;
+ background: rgba(255,255,255,.94); color: var(--ink); font: 12px/1.2 var(--font,sans-serif);
+}
+.panel-search-input:focus { outline: 2px solid rgba(40,112,180,.28); border-color: rgba(40,112,180,.58); }
+.panel-search-status { min-width: 23px; color: rgba(48,74,112,.86); font: 700 10px/1 system-ui,sans-serif; text-align: center; }
+.panel-search-next {
+ width: 32px; height: 32px; padding: 0; border: 1px solid rgba(82,128,190,.34); border-radius: 9px;
+ background: rgba(255,255,255,.94); color: #315f91; font: 800 17px/1 system-ui,sans-serif; cursor: pointer;
+}
+.panel-search-next:disabled { opacity: .36; cursor: default; }
+.panel-search-match {
+ position: relative;
+ outline: 3px solid rgba(255,190,38,.72) !important;
+ outline-offset: 1px;
+ background-color: rgba(255,235,135,.62) !important;
+ scroll-margin-block: 150px 80px;
+}
+.panel-search-current {
+ outline-color: rgba(225,91,38,.92) !important;
+ box-shadow: 0 0 0 4px rgba(255,160,70,.22) !important;
+}
+
+/* v39.16.40: header search uses partial matching and bidirectional navigation. */
+.top-panel-search {
+ flex: 0 1 270px;
+ width: clamp(190px, 21vw, 270px);
+ min-width: 190px;
+ display: grid;
+ grid-template-columns: minmax(92px, 1fr) auto 30px 30px;
align-items: center;
gap: 4px;
- margin-left: 4px;
+ margin: 0 2px 0 0;
+ padding: 0;
+ border: 0;
+}
+.top-panel-search .panel-search-input { height: 36px; }
+.panel-search-prev,
+.panel-search-next {
+ width: 30px;
+ height: 32px;
+ padding: 0;
+ border: 1px solid rgba(82,128,190,.34);
+ border-radius: 9px;
+ background: rgba(255,255,255,.94);
+ color: #315f91;
+ font: 800 16px/1 system-ui,sans-serif;
+ cursor: pointer;
+}
+.panel-search-prev:disabled,
+.panel-search-next:disabled { opacity: .36; cursor: default; }
+
+/* Match the expanded colony settings surface to the colony graph surface. */
+.colony-card .colony-setting-fold[open] {
+ background: linear-gradient(180deg, rgba(255,255,255,0.70), rgba(246,240,228,0.54));
+}
+
+
+/* Shared in-game confirmations */
+.game-confirm-dialog { z-index: 16000; }
+.game-confirm-panel { width: min(460px, 100%); }
+.game-confirm-message { white-space: pre-line; }
+.game-confirm-actions { justify-content: flex-end; }
+.game-confirm-actions .btn { width: auto; }
+
+/* Achievements */
+.achievement-top-btn {
+ border-color: rgba(177, 132, 38, .38);
+ color: #72521a;
+ background: linear-gradient(180deg, rgba(255, 251, 224, .96), rgba(247, 229, 153, .88));
+ font-weight: 800;
+}
+.achievement-top-btn.complete {
+ border-color: rgba(105, 158, 63, .44);
+ color: #456729;
+ background: linear-gradient(180deg, rgba(245, 255, 229, .98), rgba(213, 239, 173, .92));
+}
+.achievement-dialog-panel { width: min(720px, 100%); }
+.achievement-dialog-head {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 14px;
+}
+.achievement-dialog-head h2 { margin-bottom: 5px; }
+.achievement-dialog-kicker {
+ margin-bottom: 3px;
+ color: var(--muted);
+ font-size: 10px;
+ font-weight: 800;
+ letter-spacing: .08em;
+}
+.achievement-close-icon {
+ width: 34px;
+ height: 34px;
+ border: 1px solid rgba(84, 68, 48, .16);
+ border-radius: 999px;
+ background: rgba(255, 255, 255, .76);
+ color: var(--ink);
+ font-size: 22px;
+ line-height: 1;
+ cursor: pointer;
+}
+.achievement-shared-status {
+ margin: 10px 0;
+ padding: 8px 10px;
+ border: 1px solid rgba(104, 86, 61, .12);
+ border-radius: 10px;
+ background: rgba(241, 235, 222, .54);
+ color: var(--muted);
+ font-size: 11px;
+}
+.achievement-list { display: grid; gap: 10px; }
+.achievement-group {
+ overflow: hidden;
+ border: 1px solid rgba(105, 87, 60, .15);
+ border-radius: 13px;
+ background: rgba(247, 243, 234, .72);
+}
+.achievement-group-summary {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ padding: 10px 12px;
+ color: #554b3e;
+ cursor: pointer;
+ list-style: none;
user-select: none;
}
-.temperature-slider {
- width: 100%;
- margin-top: 8px;
+.achievement-group-summary::-webkit-details-marker { display: none; }
+.achievement-group-summary::before {
+ content: "\25b6";
+ flex: 0 0 auto;
+ color: #8b7657;
+ font-size: 10px;
+ transition: transform .16s ease;
}
-.temperature-slider:disabled {
- opacity: 0.52;
+.achievement-group[open] > .achievement-group-summary::before { transform: rotate(90deg); }
+.achievement-group-summary strong { margin-right: auto; font-size: 13px; }
+.achievement-group-count {
+ color: var(--muted);
+ font-size: 10px;
+ font-weight: 800;
+ font-variant-numeric: tabular-nums;
}
-
-.colony-limit-control {
- margin: 8px 0 4px;
- padding: 10px 11px;
- border: 1px solid rgba(118, 92, 61, 0.16);
- border-radius: 14px;
- background: rgba(255, 251, 244, 0.56);
+.achievement-group-body {
+ display: grid;
+ gap: 9px;
+ padding: 0 9px 9px;
}
-.colony-limit-head {
+.achievement-group-empty {
+ margin: 0;
+ padding: 12px;
+ border-radius: 10px;
+ background: rgba(255, 255, 255, .58);
+ color: var(--muted);
+ font-size: 11px;
+}
+.achievement-entry {
+ --achievement-rate: 0%;
+ position: relative;
+ isolation: isolate;
+ overflow: hidden;
+ display: grid;
+ grid-template-columns: 42px minmax(0, 1fr);
+ gap: 10px;
+ align-items: center;
+ min-height: 62px;
+ padding: 9px 11px;
+ border: 1px solid rgba(105, 87, 60, .15);
+ border-radius: 13px;
+ background: linear-gradient(180deg, rgba(255, 255, 255, .88), rgba(244, 238, 226, .78));
+ cursor: default;
+}
+.achievement-entry:focus-visible {
+ outline: 2px solid rgba(85, 119, 149, .58);
+ outline-offset: 2px;
+}
+.achievement-entry::before {
+ content: "";
+ position: absolute;
+ z-index: 0;
+ inset: 0 auto 0 0;
+ pointer-events: none;
+ width: var(--achievement-rate);
+ background: linear-gradient(90deg, rgba(129, 158, 188, .27), rgba(156, 184, 207, .17));
+ border-right: 1px solid rgba(85, 119, 149, .20);
+ transition: width .32s ease;
+}
+.achievement-entry > * {
+ position: relative;
+ z-index: 1;
+}
+.achievement-entry.unlocked {
+ border-color: rgba(173, 129, 35, .38);
+ background: linear-gradient(180deg, rgba(255, 253, 235, .96), rgba(248, 231, 163, .70));
+}
+.achievement-entry.unlocked::before {
+ background: linear-gradient(90deg, rgba(229, 179, 51, .34), rgba(244, 211, 102, .22));
+ border-right-color: rgba(173, 129, 35, .28);
+}
+.achievement-entry-icon {
+ width: 38px;
+ height: 38px;
+ display: grid;
+ place-items: center;
+ border-radius: 11px;
+ background: rgba(106, 91, 68, .09);
+ color: #8b7657;
+ font-size: 22px;
+}
+.achievement-entry.unlocked .achievement-entry-icon {
+ background: rgba(242, 196, 74, .22);
+ color: #b67b13;
+}
+.achievement-entry-copy { min-width: 0; }
+.achievement-entry-heading {
display: flex;
align-items: baseline;
+ gap: 9px;
+ min-width: 0;
+}
+.achievement-entry-copy strong {
+ flex: 0 0 auto;
+ display: block;
+ font-size: 14px;
+ line-height: 1.35;
+}
+.achievement-entry-description {
+ min-width: 0;
+ color: #665d50;
+ font-size: 11px;
+ line-height: 1.45;
+}
+.achievement-entry.locked .achievement-entry-description {
+ color: #8b8378;
+ font-style: italic;
+}
+.achievement-entry-progress {
+ margin-top: 4px;
+ color: #526b82;
+ font-size: 10px;
+ font-weight: 800;
+ font-variant-numeric: tabular-nums;
+}
+.achievement-entry-meta {
+ display: flex;
justify-content: space-between;
- gap: 8px;
- color: rgba(68, 55, 41, 0.84);
- font-size: 12px;
+ flex-wrap: wrap;
+ gap: 6px 12px;
+ margin-top: 5px;
+ color: var(--muted);
+ font-size: 10px;
+ font-weight: 700;
}
-.colony-limit-head small { color: var(--muted); font-size: 10px; }
-.colony-limit-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 9px; margin-top: 8px; }
-.colony-limit-row { display: grid; grid-template-columns: minmax(0, 1fr) 54px; gap: 5px 7px; align-items: center; min-width: 0; font-size: 11px; color: var(--muted); }
-.colony-limit-row span { grid-column: 1 / -1; min-width: 0; }
-.colony-limit-slider { width: 100%; min-width: 0; accent-color: rgba(84, 138, 72, 0.92); }
-.colony-limit-number { width: 100%; min-width: 0; box-sizing: border-box; border: 1px solid rgba(100, 78, 52, 0.22); border-radius: 8px; padding: 5px 6px; background: rgba(255,255,255,0.78); color: var(--ink); font: inherit; text-align: right; }
-.colony-limit-status { margin-top: 7px; color: rgba(68, 55, 41, 0.78); font-size: 10px; line-height: 1.35; }
+.achievement-entry-percentage {
+ flex: 0 0 auto;
+ margin-left: auto;
+ color: #5d6570;
+ font-variant-numeric: tabular-nums;
+}
+@media (max-width: 620px) {
+ .achievement-entry-heading { display: block; }
+ .achievement-entry-description { display: block; margin-top: 2px; }
+}
+.achievement-dialog-actions { justify-content: flex-end; flex-wrap: wrap; }
+.achievement-dialog-actions .btn { width: auto; }
+.achievement-dialog-actions .achievement-reset-btn { margin-right: auto; }
+.achievement-toast {
+ position: fixed;
+ top: 18px;
+ left: 18px;
+ right: auto;
+ z-index: 14000;
+ width: min(360px, calc(100vw - 36px));
+ display: grid;
+ grid-template-columns: 44px minmax(0, 1fr);
+ gap: 10px;
+ align-items: center;
+ padding: 11px 13px;
+ border: 1px solid rgba(184, 137, 37, .42);
+ border-radius: 14px;
+ background: rgba(255, 249, 220, .97);
+ color: #57401d;
+ box-shadow: 0 16px 44px rgba(45, 34, 20, .24);
+ transform-origin: center;
+ transition: opacity .18s ease, transform .18s ease, filter .18s ease, visibility .18s ease;
+}
+.achievement-toast.absorbing {
+ opacity: 0;
+ transform: translate(var(--achievement-toast-dx, 0px), var(--achievement-toast-dy, 0px)) scale(.08);
+ filter: blur(2px) saturate(1.4);
+ border-radius: 999px;
+ pointer-events: none;
+ transition: opacity .72s ease-in, transform .72s cubic-bezier(.55, .02, .86, .3), filter .72s ease-in, border-radius .72s ease-in;
+}
+.achievement-top-btn.achievement-absorb-target {
+ animation: achievement-target-pulse .22s ease-out .54s both;
+}
+@keyframes achievement-target-pulse {
+ 0% { transform: scale(1); }
+ 45% { transform: scale(1.09); }
+ 100% { transform: scale(1); }
+}
+.achievement-toast.hidden {
+ opacity: 0;
+ visibility: hidden;
+ pointer-events: none;
+ transform: translateY(-10px);
+}
+.achievement-toast-icon {
+ width: 40px;
+ height: 40px;
+ display: grid;
+ place-items: center;
+ border-radius: 12px;
+ background: #efc34c;
+ color: #fff8d8;
+ font-size: 24px;
+}
+.achievement-toast-copy { min-width: 0; }
+.achievement-toast-copy small { display: block; margin-bottom: 2px; color: #8a6b2c; font-size: 10px; font-weight: 800; }
+.achievement-toast-copy strong { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 14px; }
+@media (max-width: 980px) {
+ .achievement-toast {
+ top: calc(env(safe-area-inset-top, 0px) + 10px);
+ left: 12px;
+ right: auto;
+ width: min(360px, calc(100vw - 24px));
+ }
+ .achievement-dialog-actions { flex-direction: column-reverse; }
+ .achievement-dialog-actions .btn { width: 100%; }
+}
-/* v39.15.131: keep the folded tool card usable when other right-side cards are expanded. */
-.tool-card.collapsed {
- min-height: 64px !important;
- height: auto !important;
- flex: 0 0 auto !important;
- flex-basis: auto !important;
- overflow: visible !important;
-}
-.tool-card.collapsed .panel-card-toggle {
- min-height: 46px;
-}
-.tool-card.collapsed .panel-card-body {
- display: none !important;
+@media (prefers-reduced-motion: reduce) {
+ .achievement-toast.absorbing { transition-duration: .12s; }
+ .achievement-top-btn.achievement-absorb-target { animation: none; }
}
+
+/* v39.16.56: keep the upper-limit heading on one line in the compact panel. */
+.colony-limit-section .colony-setting-section-head > span { font-size: 10.8px; white-space: nowrap; }
+.colony-limit-section .colony-setting-section-head > small { font-size: 9px; white-space: nowrap; }
diff --git a/css/layout.css b/css/layout.css
index 6515c70..18545dc 100644
--- a/css/layout.css
+++ b/css/layout.css
@@ -23,13 +23,13 @@
.tool[data-tool="undo"].tool-emoji-icon::before,
.tool[data-tool="redo"].tool-emoji-icon::before { font-size: 29px; font-family: var(--font, sans-serif); }
.tool[data-tool="delete"] { --tool-icon-zoom: 1.42; }
-.tool[data-tool="area_delete"] { --tool-icon-zoom: 1.20; }
+.tool[data-tool="area_delete"] { --tool-icon-zoom: 1; }
.tool[data-tool="pinch"].tool-emoji-icon::before { font-size: 22px; }
.tool[data-tool="area_delete"].tool-emoji-icon::before {
font-size: 31px;
line-height: 1;
- color: #5d7fb9;
- text-shadow: 0 1px 0 rgba(255,255,255,0.65), 0 2px 5px rgba(146,54,62,0.16);
+ color: #b14f58;
+ text-shadow: 0 1px 0 rgba(255,255,255,0.75), 0 2px 5px rgba(146,54,62,0.14);
filter: none;
transform: translateY(-54%) scale(var(--tool-icon-zoom, 1));
}
@@ -114,16 +114,17 @@
position: absolute;
right: 4px;
bottom: 3px;
- min-width: 16px;
+ min-width: 22px;
padding: 1px 4px;
border-radius: 999px;
background: rgba(42,36,29,0.72);
color: #fff;
- font-size: 8.5px;
+ font-size: 9.5px;
line-height: 1;
text-align: center;
box-shadow: 0 1px 4px rgba(42,36,29,0.16);
- pointer-events: none;
+ pointer-events: auto;
+ cursor: pointer;
}
.chart-tabs { display: grid; grid-template-columns: repeat(3, 1fr); gap: 5px; margin: 3px 0 8px; }
.chart-tabs button { border: 1px solid var(--line); background: rgba(255,255,255,0.62); border-radius: 999px; padding: 5px 4px; font-size: 11px; cursor: pointer; }
@@ -307,3 +308,42 @@ meter {
@media (min-width: 921px) {
.mobile-mode-controls { display: none !important; }
}
+
+
+
+.tool-pick-mode-control.tool-emoji-icon::before {
+ opacity: 0.18;
+ color: #3b78a0;
+ font-size: 29px;
+ filter: none;
+}
+
+.tool-pick-mode-control {
+ --tool-icon-zoom: 1;
+ background: linear-gradient(180deg, rgba(255,255,255,0.92), rgba(235,246,255,0.88));
+ border-color: rgba(48,126,176,0.34);
+}
+.tool-pick-mode-control .tool-pick-mode-value {
+ position: absolute;
+ left: 50%;
+ right: auto;
+ bottom: 5px;
+ transform: translateX(-50%);
+ min-width: 38px;
+ padding: 3px 6px;
+ border-radius: 999px;
+ z-index: 4;
+ background: rgba(25,85,128,0.94);
+ color: #fff;
+ font-size: 8.5px;
+ line-height: 1;
+ text-align: center;
+ box-shadow: 0 1px 4px rgba(25,85,128,0.18);
+}
+.tool-pick-mode-control[data-pick-mode="grid20"],
+.tool-pick-mode-control[data-pick-mode="grid40"],
+.tool-pick-mode-control[data-pick-mode="grid60"] {
+ background: linear-gradient(180deg, #ffffff, #eaf7ff);
+ border-color: rgba(38,132,190,0.56);
+ box-shadow: inset 0 0 0 1px rgba(84,176,226,0.20), 0 8px 20px rgba(59,133,177,0.10);
+}
diff --git a/css/mobile.css b/css/mobile.css
index ca86dcc..fa953da 100644
--- a/css/mobile.css
+++ b/css/mobile.css
@@ -397,3 +397,35 @@ body.touch-mobile-ui.mobile-mode-tool #gameCanvas { cursor: crosshair; }
right: max(10px, env(safe-area-inset-right, 0px));
}
}
+
+/* v39.16.40: selected tool descriptions appear at the top on touch devices. */
+body.touch-mobile-ui .tool-tip-popover.mobile-selection-tip {
+ top: calc(env(safe-area-inset-top, 0px) + 8px) !important;
+ left: 50% !important;
+ transform: translateX(-50%);
+ width: min(680px, calc(100vw - 18px));
+ max-width: calc(100vw - 18px);
+ padding: 10px 12px;
+ border-radius: 12px;
+ background: rgba(46,38,30,.90);
+ font-size: 12px;
+ line-height: 1.45;
+ text-align: left;
+}
+@media (max-width: 980px) {
+ .top-actions { overflow: visible; }
+ .top-menu { flex: 0 0 auto; }
+ .top-menu-panel { position: fixed; top: calc(env(safe-area-inset-top, 0px) + 58px); right: max(10px, env(safe-area-inset-right, 0px)); }
+}
+
+@media (max-width: 980px) {
+ .top-panel-search {
+ flex: 0 0 220px;
+ width: 220px;
+ min-width: 220px;
+ }
+ .top-panel-search .panel-search-input { height: 34px; }
+}
+@media (max-width: 980px) {
+ .top-actions { overflow-x: auto; overflow-y: hidden; }
+}
diff --git a/index.html b/index.html
index d5a696f..4ef0673 100644
--- a/index.html
+++ b/index.html
@@ -4,14 +4,14 @@
たりない観察
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
@@ -33,23 +33,46 @@
+
+
+ 0
+
+
+
-
-