This commit is contained in:
33333-33333 2026-06-03 18:47:09 +09:00
commit 06ab438d7b
13 changed files with 1167 additions and 3747 deletions

View file

@ -1,36 +0,0 @@
# Default Art Audit
## 実施内容
`app.js``buildDefaultGalleryPack()` を確認し、初期同梱作品の品質を監査した。低品質または初期作品として弱いものは、削除対象リストで除外する方式にした。既存ローカル状態に残っている Island Team 製の該当作品も、次回ロード時に Collection / island placement / dynamic summon から除外される。
## 削除した初期作品
| 作品名 | 理由 |
|---|---|
| Shell Rock | 16×16に対して実質的な形状が小さく、島上で石・貝の識別点が弱い。 |
| Azure Minnow | 魚としての頭・尾・ヒレが弱く、単なる水色の塊に近い。 |
| Firefly Swirl | 8×8で透明部が多く、作品というより粒子エフェクトに近い。 |
| Leaf Sparrow | 鳥のシルエットが薄く、頭・翼・尾の分離が不足している。 |
| Silver Trout | 魚としては読めるが、既存の Cloud Koi / Sunset Koi より低密度で初期作品として弱い。 |
| Butterfly Fish | 小型魚としての情報量が少なく、島上で主題が読み取りにくい。 |
## 残したが改変候補に入る作品
| 作品名 | コメント |
|---|---|
| River Stones | 8×8としては成立しているが、石の影と水際の情報を足すと良い。 |
| Paper Crane | モチーフは明確だが、分断セルが多い。羽根の接続を調整できる。 |
| Linen Cottage | 読みやすいが、屋根と壁の面が単純。影と窓を増やせる。 |
| Human 系小型作品 | 役割差はあるが、体型・顔・道具の差分が弱いものがある。 |
## コード上の変更点
- `REMOVED_LOW_QUALITY_SEED_ASSET_NAMES` を追加。
- `buildDefaultGalleryPack()` の戻り値に品質フィルタを適用。
- `mergeDefaultGallery()` で既存ローカル状態の Island Team 製削除対象も除外。
- Python 側テストは `14 passed`
## 次の作業
ユーザー確認後、新しいデフォルト作品を追加する。追加時は `PIXEL_ART_QUALITY.md` の基準に従い、既存の弱いカテゴリを補う。

View file

@ -1,35 +0,0 @@
# Default Art Expansion / Depth Lighting Tuning
## Depth tuning
Adjusted the depth-local light response so that:
- local light influence stays colorized instead of washing into a neutral gray band,
- the light source itself and nearby sprite/depth corners catch the strongest highlight,
- brightness depends less on raw source-to-pixel distance than before,
- convex edges still read clearly under nearby inspection/night lights.
Implementation areas: `depthLocalLightResponse()`, `getDepthEdgeLightBoost()`, and local-light color mixing inside `shadeAssetPixelColor()` in `app.js`.
## UI bubble correction
- Reduced the selection bubble top offset so the bubble appears closer to the selected sprite.
## New large default works
### Static / building
- Smile Portrait — 24x32
- Rosetta Stela — 24x32
- Pharaoh Mask — 24x32
- Terracotta Sentinel — 20x32
### Static / nature
- Great Wave Panel — 32x24
- Sunflower Still Life — 24x32
- Stone Circle — 32x24
## Notes
- `assets.json` and `default_assets_sheet.png` were refreshed.
- The seeded island placement list was expanded so these works appear by default.
- Save schema was bumped to `25`.

View file

@ -1,69 +0,0 @@
# ドット絵品質基準 / Pixel Art Quality Standard
このプロトタイプで「良質なドット絵」と見なす基準。共有サービスの初期作品、審査、改変、削除判断で使う。
## 1. 最低条件
- **透明セルを作品の一部として設計している**:余白はただの空きではなく、シルエットを読ませるために残す。
- **1秒以内に主題が判別できる**:縮小表示、島上表示、夜間表示でも「家」「鳥」「魚」「橋」などの役割が読める。
- **接地・重心が明確**:静物は地面や水面に乗って見える。動物・人は足元または接地点が曖昧でない。
- **パレットに収まる**:任意色の多用ではなく、既存パレットコードに近い色で統一する。
- **クリック対象として破綻しない**:極端に薄い線、孤立点だけの作品、透明部分が多すぎる作品は避ける。
## 2. 具体的な評価軸
| 評価軸 | 良い状態 | 低品質と判断する状態 |
|---|---|---|
| シルエット | 背景なしでも輪郭だけで主題が読める | 点・棒・塊にしか見えない |
| 明暗 | 上面・側面・影の方向がある | 全面が同じ明度で平坦 |
| 色数 | 小物は3〜6色、中型は5〜10色程度で役割が分かれる | 1〜2色だけ、または無秩序に多色 |
| ピクセル密度 | 余白、密部、抜きのバランスがある | ほぼ空白、またはベタ塗りの矩形 |
| エッジ | 角、段差、斜線に意図がある | ガタつきが偶然に見える |
| 内部ディテール | 目、窓、葉、波、道具など識別点がある | 主題を補強する点がない |
| 島との親和性 | 地形、昼夜、他作品のサイズ感と合う | 尺度が浮く、接地しない、光源が不自然 |
| メタ情報 | building の door、light、depth、particle が主題に沿う | メタ情報がない、または見た目と矛盾する |
## 3. サイズ別の合格目安
### 8×8
- 主題は1つに絞る。
- 色は3〜5色までを目安にする。
- 透明を含めても、主題の有効ピクセルは概ね16セル以上ほしい。
- 「点の集合」ではなく、頭・胴・尾、花・葉・茎などの構造を最低2部位に分ける。
### 12×12〜16×16
- デフォルト作品の中心サイズ。
- 輪郭色、主色、中間色、ハイライト、影の5層を基本にする。
- 動物・人は顔または進行方向が読めること。
- 建物・自然物は接地線、屋根/幹/水面などの基礎形を入れる。
### 20×20以上
- 大型作品はディテール量だけでなく、遠目の大形状を優先する。
- 3〜5個の大きな面に分け、その中に小ディテールを置く。
- 光・depth・particle を使う場合、効果が主題を補強していること。
## 4. 削除・改変の判断
### 削除候補
- 何を描いたか判別できない。
- 既存の高品質作品と役割が重複し、見劣りする。
- ほぼ点、線、単色塊で、改変より作り直しが早い。
- 島上でクリックや配置の視認性を落とす。
### 改変候補
- 主題は読めるが、明暗・輪郭・接地が弱い。
- 色の階調が足りない。
- サイズに対してディテールが不足している。
- 良いアイデアはあるが、他作品群と密度が合っていない。
## 5. デフォルト作品としての追加基準
- 初回表示でサービスの品質水準を示せること。
- ユーザーがリミックスしたくなる明確なモチーフを持つこと。
- 小型、中型、大型、静物、動物、人、船、水辺、建物が偏りすぎないこと。
- 低品質作品を大量に残して作品数を稼がないこと。

View file

@ -20,7 +20,7 @@ Open `index.html` in a modern browser. No build step or third-party dependency i
- Save assets to the local collection.
- Place static assets and summon dynamic assets onto the island.
- Dynamic assets default to right-facing on spawn; left-facing is rendered by mirroring during movement only.
- Local voting, hiding, remix/copy edit, and import/export tools are included.
- Local voting, hiding, remix/copy edit, and collection management are included.
## Phase 1 changes
@ -33,7 +33,6 @@ Open `index.html` in a modern browser. No build step or third-party dependency i
- Local save uses a compact Phase 2 envelope in `localStorage`.
- Pixel planes are bbox-cropped and RLE-compressed when this is smaller than raw cropped data.
- Compact asset bundles can be exported separately from world snapshots.
- World snapshots contain placements, dynamic summons, and an asset manifest rather than full asset blobs.
- Local object/asset mutations append small sync events to `eventLog`.
- IndexedDB asset/snapshot cache helpers are included for future server-backed missing-asset fetches.
@ -46,7 +45,6 @@ Open `index.html` in a modern browser. No build step or third-party dependency i
- Dynamic runtime updates are stepped at 15 Hz and capped per frame to avoid spiral-of-death on slow devices.
- Rendering pauses while the browser tab is hidden and resumes on visibility change.
- Tile/object lookups use a small spatial index for inspect, erase, and nearby-target searches.
- Data tab reports visible terrain chunks for quick performance inspection.
## Phase 4 changes
@ -55,20 +53,11 @@ Open `index.html` in a modern browser. No build step or third-party dependency i
- Added Select tool. Drag to select a rectangular area, then drag the selection or use arrow keys/buttons to move it.
- Nudge buttons move the active selection; without a selection they shift the whole drawing.
- Added horizontal/vertical flip, palette-colored outline generation, and clear selection.
- Added editor PNG export/import. PNG import quantizes the image to the current island palette and current canvas size.
- Added keyboard shortcuts: B/E/F/I/L/R/S for tools, Ctrl/Cmd+Z/Y for history, arrows for moving selected pixels, Escape to clear selection.
## Export formats
- **Export full JSON**: human-readable full local state.
- **Export compact**: compact local state suitable for saving/transferring.
- **Export snapshot**: placement state plus asset manifest; requires asset bundles or cache to render fully.
- **Export asset bundle**: compact asset blob list for missing-asset transfer.
## Phase 4 follow-up adjustments
- PNG import now chooses the editor canvas size from the image size. Square 8/16/32/64 PNGs import at their native size; other sizes are fit into the nearest supported square canvas.
- Depth is now three-state: high, normal, and low. High brightens sprite pixels; low darkens them.
- Right-clicking the world canvas clears the current map selection instead of moving or placing the selected asset.
- Dynamic sprites now pause intermittently instead of walking continuously.
@ -80,16 +69,7 @@ Open `index.html` in a modern browser. No build step or third-party dependency i
- Editor schema is now 7.
- Fixed the Advanced Draw depth controls so `Depth / High / Low / Clear` wrap inside the drawer instead of overflowing to the right.
- Added Phase 5 guardrails in the Data tab:
- local asset/object volume counters,
- duplicate-content count,
- orphan placement checks,
- terrain compatibility checks,
- hidden/report counters.
- Added `Validate world` export for pre-server integrity checks.
- Added local object reporting from the selection bubble. Reported objects are hidden locally and stored in a local moderation report log.
- Added `Export moderation report` for review/debug data.
- Added `Clear reports` to clear local report logs without un-hiding already hidden objects.
- Added soft local guardrail limits: 160 assets and 220 world objects. Existing objects can still be moved; new saves/placements are blocked when the local cap is reached.
## Phase 5 follow-up changes
@ -129,7 +109,6 @@ Open `index.html` in a modern browser. No build step or third-party dependency i
- Walking particles are less frequent and use more visibly varied colors.
- Remix always creates a new derivative asset; Edit updates the user's own original asset.
- Advanced Draw now includes configurable particle emitter cells for any asset.
- PNG export/import moved to the Data tab.
- Nudge arrow buttons under the canvas were removed; keyboard arrow nudging remains.
- UI text is larger overall, while palette color-code labels keep their small size.
@ -138,7 +117,7 @@ Open `index.html` in a modern browser. No build step or third-party dependency i
This build adds count-based island rotation.
- Default island display cap: 250 objects.
- The local display cap is adjustable from Data > Visual settings.
- The local display cap is adjustable from Settings.
- Objects are not deleted when the cap is exceeded; the effective oldest objects are omitted from the island display.
- Upvotes delay rotation-out by slot adjustment. Downvotes advance rotation-out.
- Local publish / republish quota: 5 objects per author per rolling hour.
@ -306,7 +285,6 @@ Use this to compare minified JSON, gzip, zlib, and Brotli when available. Produc
- Reworked depth local-light behavior so the source itself and nearby outer/depth corners catch the strongest highlights, while distance has a weaker effect on brightness than before.
- Reduced the selection bubble offset so the bubble sits noticeably closer to the top of the selected artwork.
- Added several new large default works themed around masterpieces, heritage, and artifacts: **Smile Portrait**, **Great Wave Panel**, **Sunflower Still Life**, **Rosetta Stela**, **Pharaoh Mask**, **Stone Circle**, **Terracotta Sentinel**.
- Refreshed `assets.json` and `default_assets_sheet.png` after the additions.
## Custom update 4: cursor-light smoothing + gray-bloom hotfix

1312
app.js

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

View file

@ -1,24 +0,0 @@
const fs = require('fs');
const vm = require('vm');
const path = require('path');
let code = fs.readFileSync(path.join(__dirname, 'app.js'), 'utf8');
code = code.replace(/\n\s*bootstrap\(\);\s*\n\}\)\(\);\s*$/, `\n window.__PIXEL_DEBUG__ = { state, buildDefaultGalleryPack, PALETTE, PALETTE_BY_CODE, assetWidth, assetHeight };\n})();`);
function makeCtx(){
const noop=()=>{};
return new Proxy({imageSmoothingEnabled:false, canvas:{width:1,height:1}}, {get(t,p){
if(p in t) return t[p];
if(p==='measureText') return () => ({width:0});
return noop;
}, set(t,p,v){ t[p]=v; return true; }});
}
function makeEl(id){ return new Proxy({ id, width:640, height:480, style:{}, classList:{add(){},remove(){},toggle(){},contains(){return false;}}, dataset:{}, value:'', checked:false, textContent:'', innerHTML:'', append(){}, appendChild(){}, addEventListener(){}, removeEventListener(){}, setAttribute(){}, removeAttribute(){}, focus(){}, blur(){}, getBoundingClientRect(){return {left:0,top:0,width:640,height:480};}, getContext(){return makeCtx();}, querySelector(){return makeEl('q');}, querySelectorAll(){return [];}}, {get(t,p){ if(p in t) return t[p]; if(p==='children') return []; return undefined; }, set(t,p,v){ t[p]=v; return true; }}); }
const localStorage = { getItem(){return null;}, setItem(){}, removeItem(){} };
const sandbox = { console, window: {}, document: { getElementById: makeEl, createElement: makeEl, querySelector(){return makeEl('q')}, querySelectorAll(){return []}, addEventListener(){}, body: makeEl('body')}, localStorage, performance: { now: () => 0 }, Math, Date, Map, Set, Array, Object, JSON, Number, String, Boolean, RegExp, Error, Uint8Array, Int8Array, Uint16Array, URL: {createObjectURL(){return ''}, revokeObjectURL(){}}, Blob: function(){}, Image: function(){}, FileReader: function(){}, requestAnimationFrame(){}, cancelAnimationFrame(){}, setTimeout, clearTimeout, structuredClone: global.structuredClone };
sandbox.window = Object.assign(sandbox.window, { PixelIslandModules:{}, devicePixelRatio:1, addEventListener(){}, removeEventListener(){}, matchMedia(){return {matches:false, addEventListener(){}, removeEventListener(){}}}, requestAnimationFrame(){}, cancelAnimationFrame(){}, document: sandbox.document, localStorage: sandbox.localStorage });
vm.createContext(sandbox);
vm.runInContext(code, sandbox, {filename:'app.js'});
const debug = sandbox.window.__PIXEL_DEBUG__;
const assets = debug.state.assets.map(a => ({id:a.id,name:a.name,category:a.category,subtype:a.subtype,width:a.width,height:a.height,pixels:a.pixels,faces:a.faces,meta:a.meta,contentHash:a.contentHash}));
fs.writeFileSync(path.join(__dirname,'assets.json'), JSON.stringify(assets,null,2));
console.log('assets', assets.length);
console.log(assets.map(a=>`${a.name} ${a.width}x${a.height} ${a.category}/${a.subtype}`).join('\n'));

View file

@ -34,7 +34,7 @@
</label>
</div>
<button id="createAccount" class="miniButton" type="button">Generate account</button>
<small id="accountNote">Save + Place creates a local account for island publishing.</small>
<small id="accountNote">Publish creates a local account for island publishing.</small>
</div>
</header>
@ -59,7 +59,6 @@
<button class="tab active" data-tab="draw">Create</button>
<button class="tab" data-tab="library">Collection</button>
<button class="tab" data-tab="settings">Settings</button>
<button class="tab devOnly" data-tab="data">Data</button>
</nav>
<div class="drawerBody">
@ -75,6 +74,7 @@
<option value="human">Human</option>
<option value="animal">Animal</option>
<option value="bird">Bird</option>
<option value="fish">Fish</option>
<option value="nature">Nature</option>
<option value="building">Building</option>
<option value="ship">Ship</option>
@ -91,7 +91,12 @@
<div class="paintLayout">
<div class="paintMainColumn">
<canvas id="paintCanvas" width="512" height="512" aria-label="Pixel editor"></canvas>
<div class="toolRow editorToolRow basicEditorTools">
<div class="palettePanel">
<div id="paletteGrid" class="paletteGrid" aria-label="Color palette"></div>
</div>
</div>
<div class="drawControlPanel">
<div class="toolRow editorToolRow basicEditorTools" aria-label="Basic draw tools">
<button id="toolBrush" class="tool toolBrush active" title="B">Draw</button>
<button id="toolErase" class="tool toolErase" title="E">Erase</button>
<button id="toolFill" class="tool toolFill" title="F">Fill</button>
@ -102,56 +107,53 @@
<button id="undoPaint" class="tool toolUndo" type="button" disabled>↶ Undo</button>
<button id="redoPaint" class="tool toolRedo" type="button" disabled>↷ Redo</button>
</div>
</div>
<div class="palettePanel">
<div id="paletteGrid" class="paletteGrid" aria-label="Color palette"></div>
</div>
</div>
<div class="advancedPanel">
<div class="advancedPanelHeader">Advanced settings</div>
<div id="advancedToolGroup" class="advancedToolGroup" hidden>
<div class="toolRow advancedDrawRow">
<button id="toolLine" class="tool toolLine advancedTool" title="L">Line</button>
<button id="toolRect" class="tool toolRect advancedTool" title="R">Rect</button>
<button id="toolSelect" class="tool toolSelect advancedTool" title="S">Select</button>
<button id="toolDoor" class="tool toolDoor advancedTool buildingOnly">Door</button>
<button id="clearPaint" class="tool danger advancedTool">Clear</button>
</div>
<div class="toolRow advancedActionRow">
<button id="outlinePaint" class="tool toolOutline advancedTool" type="button">Outline</button>
<button id="flipHorizontal" class="tool toolFlipH advancedTool" type="button">Flip H</button>
<button id="flipVertical" class="tool toolFlipV advancedTool" type="button">Flip V</button>
<button id="clearSelection" class="tool toolClearSel advancedTool" type="button" disabled>Clear Sel</button>
<div class="advancedPanel">
<div class="advancedPanelHeader">Advanced draw</div>
<div id="advancedToolGroup" class="advancedToolGroup" hidden>
<div class="toolRow advancedDrawRow">
<button id="toolLine" class="tool toolLine advancedTool" title="L">Line</button>
<button id="toolRect" class="tool toolRect advancedTool" title="R">Rect</button>
<button id="toolSelect" class="tool toolSelect advancedTool" title="S">Select</button>
<button id="toolDoor" class="tool toolDoor advancedTool buildingOnly">Door</button>
<button id="clearPaint" class="tool danger advancedTool">Clear</button>
</div>
<div class="toolRow advancedActionRow">
<button id="outlinePaint" class="tool toolOutline advancedTool" type="button">Outline</button>
<button id="flipHorizontal" class="tool toolFlipH advancedTool" type="button">Flip H</button>
<button id="flipVertical" class="tool toolFlipV advancedTool" type="button">Flip V</button>
<button id="clearSelection" class="tool toolClearSel advancedTool" type="button" disabled>Clear Sel</button>
</div>
</div>
<div class="advancedRow extraAdvancedRow">
<button id="toolLight" class="tool toolLight advancedOnly" hidden>Light</button>
<button id="toolParticle" class="tool toolParticle advancedOnly" hidden>Particle</button>
<button id="toolDepth" class="tool toolDepth advancedOnly" hidden>Depth</button>
<button id="depthHigh" class="tool advancedOnly" type="button" hidden>High</button>
<button id="depthLow" class="tool advancedOnly" type="button" hidden>Low</button>
<div id="particleDirectionWrap" class="particleControl particleControlGroup advancedOnly" hidden>
<label>Direction
<select id="particleDirection">
<option value="up" selected>↑ Up</option>
<option value="down">↓ Down</option>
<option value="left">← Left</option>
<option value="right">→ Right</option>
</select>
</label>
<small id="particleRangeStatus">Particle: paint cells like Light/Depth. Shift/right-click clears a cell.</small>
</div>
</div>
<span id="advancedHint" class="hint" hidden></span>
<input id="paintColor" type="color" value="#6bd06b" hidden />
<div id="editHint" class="hint" hidden></div>
</div>
</div>
<div class="advancedRow extraAdvancedRow">
<button id="toolLight" class="tool toolLight advancedOnly" hidden>Light</button>
<button id="toolParticle" class="tool toolParticle advancedOnly" hidden>Particle</button>
<button id="toolDepth" class="tool toolDepth advancedOnly" hidden>Depth</button>
<button id="depthHigh" class="tool advancedOnly" type="button" hidden>High</button>
<button id="depthLow" class="tool advancedOnly" type="button" hidden>Low</button>
<div id="particleDirectionWrap" class="particleControl particleControlGroup advancedOnly" hidden>
<label>Direction
<select id="particleDirection">
<option value="up" selected>↑ Up</option>
<option value="down">↓ Down</option>
<option value="left">← Left</option>
<option value="right">→ Right</option>
</select>
</label>
<small id="particleRangeStatus">Particle: paint cells like Light/Depth. Shift/right-click clears a cell.</small>
</div>
</div>
<span id="advancedHint" class="hint" hidden>Advanced reveals line, rectangle, selection, flip, light, depth, and particles.</span>
<input id="paintColor" type="color" value="#6bd06b" hidden />
<div id="editHint" class="hint">Palette color is used for pixels, lights, particles, and depth marks. Shortcuts: B/E/F/I/L/R/S, Ctrl/Cmd+Z, arrows move a selection.</div>
</div>
</div>
<div class="card stack summonCard">
<div class="cardTitle">Finish</div>
<div class="actionRow finishActions">
<button id="saveAndPlace" class="primary bigPrimary"><span>Save + Place on Island</span><span id="finishQuotaBadge" class="quotaBadge finishQuotaBadge" aria-live="polite"></span></button>
<button id="saveAndPlace" class="primary bigPrimary"><span>Publish</span><span id="finishQuotaBadge" class="quotaBadge finishQuotaBadge" aria-live="polite"></span></button>
<button id="saveAsset" class="secondary">Save to Collection</button>
</div>
</div>
@ -164,7 +166,7 @@
<div class="cardTitle">Collection</div>
<button id="showHiddenAssets" class="tool" type="button">Hidden</button>
</div>
<p class="hint">Your works are permanent here. The island is a temporary exhibition: place your own new or past works; remix only copies lineage and pixels to the canvas.</p>
<p class="hint">Your works are permanent here. The island is a temporary exhibition.</p>
<div id="hiddenAssetPanel" class="hiddenAssetPanel" hidden>
<div class="cardTitle">Hidden</div>
<div id="hiddenAssetList" class="assetList compactAssetList"></div>
@ -190,36 +192,6 @@
</div>
</section>
<section id="tab-data" class="tabPanel devOnly">
<div class="card stack">
<div class="cardTitle">Local save</div>
<p class="hint">Works are durable collection works; island placements are temporary exhibition objects. Export JSON if you want to move or test a local world.</p>
<div class="actionRow wrap">
<button id="exportData">Export full JSON</button>
<button id="exportCompact">Export compact</button>
<button id="exportSnapshot">Export snapshot</button>
<button id="exportAssetBundle">Export asset bundle</button>
<button id="importData">Import JSON</button>
<button id="exportPng" type="button">Export PNG</button>
<button id="importPng" type="button">Import PNG</button>
<input id="pngImportInput" type="file" accept="image/png,image/*" hidden />
<button id="resetAll" class="danger">Reset all</button>
</div>
<div id="syncStats" class="syncStats"></div>
<textarea id="dataBox" rows="10" placeholder="Exported JSON appears here."></textarea>
</div>
<div class="card stack">
<div class="cardTitle">Phase 5 guardrails</div>
<p class="hint">Local pre-server checks for object volume, orphaned placements, terrain compatibility, hidden objects, and report logs.</p>
<div id="guardrailStats" class="syncStats"></div>
<div class="actionRow wrap">
<button id="validateWorld" type="button">Validate world</button>
<button id="exportModerationReport" type="button">Export moderation report</button>
<button id="clearReports" class="danger" type="button">Clear reports</button>
</div>
</div>
</section>
</div>
</section>
@ -245,6 +217,17 @@
</div>
<div id="toast" class="toast" hidden></div>
<div id="confirmDialog" class="reportDialog" hidden>
<div class="reportCard" role="dialog" aria-modal="true" aria-labelledby="confirmTitle">
<div id="confirmTitle" class="cardTitle">Delete work</div>
<p id="confirmMessage" class="hint"></p>
<div class="actionRow wrap reportActions">
<button id="confirmOk" class="danger" type="button">Delete</button>
<button id="confirmCancel" type="button">Cancel</button>
</div>
</div>
</div>
<div id="reportDialog" class="reportDialog" hidden>
<div class="reportCard" role="dialog" aria-modal="true" aria-labelledby="reportTitle">
<div id="reportTitle" class="cardTitle">Report object</div>

View file

@ -420,7 +420,8 @@
sync: state.sync || { lastEventId: null },
worldMode: state.worldMode === 'shared' ? 'shared' : 'local',
serverSync: state.serverSync || { lastServerEventId: null, pendingCommands: [] },
tombstones: state.tombstones || { assets: {}, objects: {} }
tombstones: state.tombstones || { assets: {}, objects: {} },
deletedSeedAssetNames: Array.isArray(state.deletedSeedAssetNames) ? state.deletedSeedAssetNames : []
};
}
@ -445,7 +446,8 @@
sync: input.sync || { lastEventId: null },
worldMode: input.worldMode === 'shared' ? 'shared' : 'local',
serverSync: input.serverSync || { lastServerEventId: null, pendingCommands: [] },
tombstones: input.tombstones || { assets: {}, objects: {} }
tombstones: input.tombstones || { assets: {}, objects: {} },
deletedSeedAssetNames: Array.isArray(input.deletedSeedAssetNames) ? input.deletedSeedAssetNames : []
};
}

View file

@ -119,6 +119,18 @@ class WorldPolicyTest(unittest.TestCase):
self.assertIn("obj-a", state["tombstones"]["objects"])
self.assertEqual(len(state["dynamicSummons"]), 1)
def test_non_owner_can_delete_asset_and_all_placements(self):
state = base_state()
allowed, _ = can_delete_asset(state, {"id": "alice"}, "asset-b")
self.assertTrue(allowed)
result = apply_command(state, {"id": "alice"}, {"type": "asset.delete", "assetId": "asset-b"}, at=100)
self.assertTrue(result["accepted"])
self.assertTrue(apply_server_event(state, result["event"]))
self.assertNotIn("asset-b", [asset["id"] for asset in state["assets"]])
self.assertEqual(state["dynamicSummons"], [])
self.assertIn("asset-b", state["tombstones"]["assets"])
self.assertIn("dyn-b", state["tombstones"]["objects"])
def test_world_phase_event_updates_authoritative_clock(self):
state = base_state()

View file

@ -88,9 +88,7 @@ def can_delete_asset(state: MutableMapping[str, Any], account: MutableMapping[st
if not asset:
return False, {"reason": "asset_not_found"}
owner = str(asset.get("ownerAccountId") or "")
if actor == owner or is_admin(account):
return True, {"reason": None, "ownerAccountId": owner}
return False, {"reason": "not_asset_owner", "ownerAccountId": owner}
return True, {"reason": None, "ownerAccountId": owner}
def is_object_tombstoned(state: MutableMapping[str, Any], object_id: str, version: int = 0) -> bool:
@ -277,7 +275,7 @@ def apply_command(state: MutableMapping[str, Any], account: MutableMapping[str,
version = int(asset.get("version") or 0) + 1
object_tombstones: List[Dict[str, Any]] = []
for _, obj in iter_objects(state):
if str(obj.get("assetId") or "") == asset_id and (is_admin(account) or owner_of_object(state, obj) == actor):
if str(obj.get("assetId") or "") == asset_id:
object_tombstones.append({"id": str(obj["id"]), "deletedAt": at or now_ms(), "deletedBy": actor, "version": int(obj.get("version") or 0) + 1})
tombstone = {"id": asset_id, "deletedAt": at or now_ms(), "deletedBy": actor, "version": version}
return _accepted(_server_event("asset.delete", actor, {"assetId": asset_id, "assetVersion": version, "tombstone": tombstone, "objectTombstones": object_tombstones}, at))

View file

@ -97,7 +97,6 @@ button { -webkit-tap-highlight-color: transparent; }
font-size: 12px;
}
.authorCard {
width: 190px;
border: 2px solid var(--line);
@ -423,7 +422,6 @@ input[type="checkbox"] { width: auto; margin: 0 8px 0 0; }
margin-bottom: 8px;
}
.editorToolRow { grid-template-columns: repeat(5, minmax(0, 1fr)); }
.editorHistoryRow { grid-template-columns: repeat(5, minmax(0, 1fr)); }
.editorMoveRow { grid-template-columns: repeat(5, minmax(0, 1fr)); }
@ -475,16 +473,53 @@ button:disabled, .tool:disabled { opacity: .45; cursor: not-allowed; transform:
.lineageNote { min-height: 18px; color: var(--muted); font-size: 12px; }
.assetList { display: grid; gap: 10px; }
.collectionToolbar {
display: grid;
gap: 8px;
padding: 10px;
border: 2px solid rgba(36,48,68,.22);
background: #f8fbff;
}
.collectionSummary {
font-size: 12px;
font-weight: 900;
color: var(--ink);
}
.collectionSearch {
width: 100%;
margin: 0;
padding: 8px 9px;
border: 2px solid rgba(36,48,68,.28);
background: #fff;
font-size: 12px;
}
.collectionFilters {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.collectionFilter {
flex: 0 0 auto;
padding: 6px 8px;
border: 2px solid rgba(36,48,68,.24);
background: #fff;
box-shadow: none;
font-size: 11px;
}
.collectionFilter.active {
border-color: var(--line);
background: #dff4ff;
}
.assetCard {
display: grid;
grid-template-columns: 68px 1fr;
gap: 10px;
border: 2px solid var(--line);
background: #fff9e8;
background: #fff;
padding: 8px;
box-shadow: 3px 3px 0 rgba(36,48,68,.12);
box-shadow: 0 2px 0 rgba(36,48,68,.08);
}
.assetCard.selected { background: #e6ffef; }
.assetCard.selected { background: #eefbf3; border-color: #3f7d58; }
.assetPreview {
width: 64px;
height: 64px;
@ -494,6 +529,25 @@ button:disabled, .tool:disabled { opacity: .45; cursor: not-allowed; transform:
}
.assetMeta strong { display: block; font-size: 14px; }
.assetMeta span { display: block; margin-top: 3px; color: var(--muted); font-size: 12px; line-height: 1.3; }
.assetStatusRow {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-top: 5px;
}
.assetMeta .assetStatus {
display: inline-flex;
width: fit-content;
margin-top: 0;
padding: 2px 6px;
border: 1px solid rgba(36,48,68,.2);
background: #f5f5f5;
color: #3d4652;
font-size: 10px;
font-weight: 900;
}
.assetMeta .assetStatus.placed { background: #e3f8e8; color: #28633e; }
.assetMeta .assetStatus.role { background: #eef2ff; color: #3e4f78; }
.assetActions {
display: flex;
flex-wrap: wrap;
@ -528,8 +582,6 @@ button:disabled, .tool:disabled { opacity: .45; cursor: not-allowed; transform:
.legend .grass { background: #8bce76; }
.legend .highland { background: #a7be80; }
#dataBox { min-height: 160px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 11px; }
.toast {
position: absolute;
z-index: 20;
@ -554,7 +606,6 @@ button:disabled, .tool:disabled { opacity: .45; cursor: not-allowed; transform:
.toolRow { grid-template-columns: 44px repeat(3, minmax(0, 1fr)); }
}
/* refinements */
.toolHud {
top: 142px;
right: 18px;
@ -670,11 +721,9 @@ button:disabled, .tool:disabled { opacity: .45; cursor: not-allowed; transform:
.sideSwitcher button { white-space: nowrap; }
.heroEditor .hint { line-height: 1.35; }
.compactEditorTop { margin-bottom: 4px; min-height: 0; }
body, button, input, select, textarea { font-smooth: never; -webkit-font-smoothing: none; }
/* latest layout fixes */
.studioDrawer { width: min(600px, calc(100vw - 24px)); }
.paintLayout {
display: grid;
@ -707,7 +756,6 @@ body, button, input, select, textarea { font-smooth: never; -webkit-font-smoothi
.paletteGrid { grid-template-columns: repeat(10, 1fr); max-height: none; }
}
/* Selection bubble + outline controls */
.palettePanel { display: grid; gap: 8px; }
.selectionBubble {
position: absolute;
@ -790,7 +838,6 @@ body, button, input, select, textarea { font-smooth: never; -webkit-font-smoothi
background: var(--sun);
}
/* Pixel-ish UI refinements */
body, button, input, select, textarea {
font-family: "Courier New", "Lucida Console", Monaco, monospace;
letter-spacing: .02em;
@ -833,7 +880,6 @@ button, .tab, .tool, .iconButton, .cardTitle, .logo {
color: var(--muted);
}
/* Simplified current UI */
body, button, input, select, textarea {
font-family: "MS Gothic", "Osaka-Mono", "DotumChe", "Courier New", ui-monospace, monospace;
letter-spacing: .02em;
@ -851,10 +897,10 @@ body, button, input, select, textarea {
width: 100%;
text-align: left;
margin: 10px 0 6px;
cursor: pointer;
cursor: default;
}
.librarySectionTitle::after {
content: " click: genre";
content: "";
float: right;
color: var(--muted);
font-size: 10px;
@ -890,7 +936,6 @@ body, button, input, select, textarea {
.advancedRow { grid-template-columns: 1fr 1fr 1fr; }
}
.syncStats {
padding: 8px 10px;
border-radius: 12px;
@ -900,7 +945,6 @@ body, button, input, select, textarea {
line-height: 1.45;
}
/* Phase 5: keep advanced depth controls inside the drawer. */
.advancedRow {
display: flex;
flex-wrap: wrap;
@ -920,9 +964,6 @@ body, button, input, select, textarea {
flex: 1 0 100%;
min-width: 0;
}
#guardrailStats {
white-space: pre-wrap;
}
.bubbleActions {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@ -931,7 +972,6 @@ body, button, input, select, textarea {
grid-column: span 2;
}
/* Phase 5 follow-up: object hidden list + report dialog + font priority. */
body, button, input, select, textarea {
font-family: "3x5 MT Pixel Font", "Press Start 2P", "PixelMplus10", "DotGothic16", "MS Gothic", "Osaka-Mono", "Courier New", ui-monospace, monospace;
}
@ -964,12 +1004,10 @@ body, button, input, select, textarea {
.reportActions button { min-width: 0; }
.assetActions button { min-width: 0; }
.toggleList { display:flex; flex-direction:column; gap:10px; }
.checkRow { display:flex; align-items:center; gap:10px; font-size:13px; color:#243044; }
.checkRow input { width:16px; height:16px; }
/* Phase 5 UI polish: larger readable controls while keeping palette code labels small. */
body, button, input, select, textarea { font-size: 15px; }
.logo { font-size: 24px; }
.subline { font-size: 14px; }
@ -1001,8 +1039,6 @@ body, button, input, select, textarea { font-size: 15px; }
.displayLimitField input { font-size: 16px; min-height: 36px; width: 110px; }
.rotationBadge { font-size: 13px; color: #4d5b72; }
/* exhibition/account polish */
.authorCard { min-width: 210px; }
.authorCard small { display:block; margin-top:6px; font-size:12px; line-height:1.35; opacity:.82; }
.miniButton { margin-top:6px; padding:6px 8px; border:2px solid #243044; background:#fff3d9; box-shadow:2px 2px 0 rgba(36,48,68,.22); cursor:pointer; font-size:12px; }
@ -1016,8 +1052,6 @@ body, button, input, select, textarea { font-size: 15px; }
.hiddenReasonText { color:#6f2f3b; font-weight:700; }
.exhibitionNote { padding:8px 10px; background:#fff3d9; border:2px solid rgba(36,48,68,.2); margin-bottom:8px; font-size:14px; line-height:1.45; }
/* compact account bar / remove right-side HUD */
.toolHud { display: none !important; }
.topHud { min-height: 50px; padding: 8px 12px; gap: 10px; align-items: center; }
.authorCard {
@ -1043,7 +1077,6 @@ body, button, input, select, textarea { font-size: 15px; }
.authorCard small { max-width: 100%; }
}
/* 2026 polish pass: make the island the visual default, keep creation and collection one click away. */
.islandTopBar {
top: 12px;
left: 50%;
@ -1084,9 +1117,6 @@ body, button, input, select, textarea { font-size: 15px; }
.topButton.primary { background: var(--mint) !important; }
.utilityControl { display: none !important; }
.edgeAdd { display: none !important; }
.devOnly { display: none !important; }
/* Beginner-first editor: show only Pencil/Erase/Fill/Pick + Undo/Redo until Advanced is enabled. */
.advancedTool { display: none !important; }
.studioDrawer.advancedTools .advancedTool { display: inline-flex !important; }
.studioDrawer.advancedTools .toolRow .advancedTool { display: block !important; }
@ -1094,10 +1124,10 @@ body, button, input, select, textarea { font-size: 15px; }
.editorHistoryRow { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; }
.studioDrawer.advancedTools .editorToolRow { grid-template-columns: repeat(5, minmax(0, 1fr)) !important; }
.studioDrawer.advancedTools .editorHistoryRow { grid-template-columns: repeat(6, minmax(0, 1fr)) !important; }
#editHint { opacity: .78; }
#editHint::after { content: " Advanced reveals line, rectangle, selection, flip, light, depth, and particles."; }
#editHint { display: none !important; }
#advancedHint { display: none !important; }
#editHint::after { content: none; }
/* Let the island breathe under the lighter navigation. */
#worldCanvas { background: #8dd7ff; }
.selectionBubble {
border-width: 2px !important;
@ -1110,7 +1140,6 @@ body, button, input, select, textarea { font-size: 15px; }
.topButton { padding: 7px 8px !important; }
}
/* v8 account, editor, and placement refinements */
.islandClickHint {
position: fixed;
left: 14px;
@ -1240,7 +1269,6 @@ body, button, input, select, textarea { font-size: 15px; }
.advancedDrawRow, .advancedActionRow { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; }
}
/* v9 menu and particle-range refinements */
.cuteMenuButton {
flex: 0 0 auto;
width: 38px;
@ -1285,7 +1313,6 @@ body, button, input, select, textarea { font-size: 15px; }
.particleRangeButtons { grid-template-columns: 1fr 1fr; }
}
/* v10 bottom DRAW access and remix bottom sheet */
.drawDockButton {
position: fixed;
z-index: 28;
@ -1379,8 +1406,6 @@ body, button, input, select, textarea { font-size: 15px; }
.islandClickHint { display: none; }
}
/* v15 analog day-night clock */
.analogClock {
--clock-rotate: 0deg;
position: fixed;
@ -1460,8 +1485,6 @@ body, button, input, select, textarea { font-size: 15px; }
.analogClock { top: 82px; right: 10px; transform: scale(.86); transform-origin: top right; }
}
/* v16 layout refinements */
.inlineField {
display: grid;
grid-template-columns: 86px minmax(0, 1fr);
@ -1496,7 +1519,6 @@ body, button, input, select, textarea { font-size: 15px; }
.analogClock { top: 8px; right: 10px; transform: scale(.72); transform-origin: top right; }
}
/* Settings tab alignment */
#tab-settings .card {
text-align: center;
}
@ -1523,7 +1545,6 @@ body, button, input, select, textarea { font-size: 15px; }
text-align: center;
}
/* v17 requested UI tightening */
.drawQuotaBadge {
position: fixed;
z-index: 27;
@ -1617,7 +1638,6 @@ body, button, input, select, textarea { font-size: 15px; }
.paletteGrid { grid-template-columns: repeat(8, minmax(26px, 1fr)) !important; }
}
/* v18 requested UI + palette corrections */
.drawDockButton,
#saveAndPlace.bigPrimary {
display: inline-flex !important;
@ -1663,7 +1683,6 @@ body, button, input, select, textarea { font-size: 15px; }
.paletteGrid { grid-template-columns: repeat(10, minmax(20px, 1fr)) !important; }
}
/* v19 requested create-layout cleanup */
.studioDrawer {
width: min(1060px, calc(100vw - 24px)) !important;
}
@ -1795,7 +1814,6 @@ body, button, input, select, textarea { font-size: 15px; }
#editHint { max-width: none; }
}
/* v20 requested refinements */
.studioDrawer {
width: min(424px, calc(100vw - 20px)) !important;
}
@ -1864,8 +1882,6 @@ body, button, input, select, textarea { font-size: 15px; }
.creationMetaRow { grid-template-columns: minmax(0, 1fr) 58px 12px 58px !important; }
}
/* v21 correction: keep palette to the right of the canvas on desktop.
The previous left-drawer shrink made the editor too narrow and forced the palette below. */
@media (min-width: 761px) {
.studioDrawer {
width: min(960px, calc(100vw - 20px)) !important;
@ -1897,8 +1913,6 @@ body, button, input, select, textarea { font-size: 15px; }
}
}
/* v22 requested: compact left drawer + Create tab header layout
Desktop drawer is capped to roughly 40% of the viewport instead of occupying most of the screen. */
@media (min-width: 761px) {
.studioDrawer {
width: clamp(440px, 40vw, 620px) !important;
@ -2022,7 +2036,6 @@ body, button, input, select, textarea { font-size: 15px; }
}
}
/* v23 requested: canvas-surroundings redesign + palette cleanup */
:root {
--editorSurface: #efe9dc;
--editorPanel: #f6f1e5;
@ -2155,18 +2168,14 @@ body, button, input, select, textarea { font-size: 15px; }
}
.advancedPanel {
border: 5px solid var(--editorAccent) !important;
background: #ececeb !important;
padding: 16px 16px 12px !important;
margin-top: 2px !important;
border: 0 !important;
background: transparent !important;
padding: 8px 0 0 !important;
margin-top: 0 !important;
}
.advancedPanelHeader {
color: #7f7d7a !important;
font-size: clamp(28px, 3vw, 54px) !important;
line-height: 1.05 !important;
letter-spacing: .01em !important;
margin: 0 0 10px !important;
display: none !important;
}
.advancedPanel .advancedDrawRow,
@ -2273,7 +2282,6 @@ body, button, input, select, textarea { font-size: 15px; }
}
}
/* v24: align canvas, primary tools, and palette exactly as one editor block */
.paintMainColumn {
display: grid !important;
grid-template-rows: auto auto auto !important;
@ -2305,7 +2313,6 @@ body, button, input, select, textarea { font-size: 15px; }
}
}
/* v25: requested layout-only cleanup. Keep existing button design; move controls around the canvas. */
:root {
--createGap: 12px;
}
@ -2526,3 +2533,308 @@ body, button, input, select, textarea { font-size: 15px; }
font-weight: 900 !important;
}
}
#tab-draw .card.editorCard.heroEditor {
padding: 8px !important;
}
.creationMetaRow {
align-items: center !important;
}
.creationMetaRow .metaInput,
.creationMetaRow select.metaInput {
height: 28px !important;
min-height: 28px !important;
padding: 3px 7px !important;
font-size: 12px !important;
line-height: 1.1 !important;
border-width: 2px !important;
}
.creationMetaRow .dimensionSeparator {
font-size: 15px !important;
}
.paintLayout {
align-items: start !important;
}
.drawControlPanel {
display: grid;
gap: 8px;
align-self: start;
min-width: 0;
}
.drawControlPanel .basicEditorTools,
.drawControlPanel .editorUtilityRow {
margin: 0 !important;
}
.drawControlPanel .advancedPanel {
margin: 0 !important;
}
#saveAndPlace.bigPrimary {
min-width: 0 !important;
max-width: 100% !important;
white-space: normal !important;
overflow-wrap: anywhere !important;
line-height: 1.15 !important;
flex-wrap: wrap !important;
row-gap: 3px !important;
padding-left: 8px !important;
padding-right: 8px !important;
}
#saveAndPlace.bigPrimary > span:first-child {
min-width: 0 !important;
}
#saveAndPlace .finishQuotaBadge {
max-width: 100% !important;
white-space: normal !important;
overflow-wrap: anywhere !important;
line-height: 1.1 !important;
font-size: 9px !important;
padding: 2px 5px !important;
}
.collectionViewTabs.tabs {
grid-template-columns: repeat(3, minmax(0, 1fr));
border: 2px solid var(--line);
box-shadow: 3px 3px 0 rgba(36,48,68,.12);
margin: 2px 0 0;
padding: 6px !important;
gap: 6px !important;
}
.collectionViewTab.tab {
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 7px 5px !important;
font-size: 12px !important;
}
@media (min-width: 761px) {
.studioDrawer {
width: clamp(520px, 42vw, 760px) !important;
}
.creationMetaRow {
grid-template-columns: minmax(120px, 1.35fr) clamp(42px, 4vw, 58px) 12px clamp(42px, 4vw, 58px) minmax(82px, .9fr) !important;
gap: 8px !important;
margin-bottom: 8px !important;
}
.paintLayout {
grid-template-columns: minmax(0, 1fr) clamp(148px, 12vw, 210px) !important;
gap: 12px !important;
margin-bottom: 10px !important;
}
.paintMainColumn {
grid-template-rows: auto auto !important;
gap: 8px !important;
}
.paintMainColumn #paintCanvas {
max-height: min(58vh, 560px) !important;
}
.paintMainColumn .palettePanel {
min-height: 0 !important;
align-self: start !important;
padding: 7px !important;
}
.paintMainColumn .paletteGrid {
grid-template-columns: repeat(10, minmax(0, 1fr)) !important;
gap: 3px !important;
}
.paintMainColumn .paletteSwatch,
.paintMainColumn .paletteGrid button {
border-width: 2px !important;
}
.drawControlPanel .basicEditorTools {
grid-template-columns: 1fr !important;
gap: 6px !important;
}
.drawControlPanel .editorUtilityRow {
grid-template-columns: 1fr !important;
gap: 6px !important;
}
.drawControlPanel .basicEditorTools .tool,
.drawControlPanel .editorUtilityRow .tool {
min-height: 36px !important;
padding: 7px 6px !important;
font-size: 12px !important;
}
.drawControlPanel .advancedPanel {
padding: 8px !important;
max-height: calc(100vh - 330px);
overflow: auto;
}
.drawControlPanel .advancedPanelHeader {
display: block !important;
font-size: 12px !important;
margin-bottom: 6px !important;
}
.advancedPanel .advancedDrawRow,
.advancedPanel .advancedActionRow,
.advancedPanel .extraAdvancedRow {
grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
gap: 5px !important;
margin-bottom: 6px !important;
}
.advancedPanel .tool {
min-height: 30px !important;
padding: 5px 4px !important;
font-size: 10px !important;
}
.advancedPanel .particleControlGroup {
grid-column: 1 / -1;
}
}
@media (min-width: 761px) and (max-height: 780px) {
.drawerHead {
padding-top: 4px !important;
padding-bottom: 3px !important;
}
.drawerHead p {
display: none;
}
.tabs {
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.drawControlPanel .advancedPanel {
max-height: calc(100vh - 285px);
}
}
@media (max-width: 760px) {
.creationMetaRow .metaInput,
.creationMetaRow select.metaInput {
height: 26px !important;
min-height: 26px !important;
font-size: 12px !important;
padding: 3px 6px !important;
}
.paintLayout {
grid-template-columns: 1fr !important;
gap: 10px !important;
}
.paintMainColumn {
grid-template-rows: auto auto !important;
}
.paintMainColumn .paletteGrid {
grid-template-columns: repeat(8, minmax(0, 1fr)) !important;
}
.drawControlPanel {
gap: 8px;
}
.drawControlPanel .advancedPanel {
margin-top: 0 !important;
}
#saveAndPlace.bigPrimary {
flex-direction: column;
}
.collectionViewTab.tab {
font-size: 11px !important;
}
}
@media (min-width: 761px) {
.studioDrawer.advancedTools .drawControlPanel .editorToolRow,
.studioDrawer.advancedTools .drawControlPanel .basicEditorTools,
.studioDrawer.advancedTools .drawControlPanel .editorUtilityRow {
grid-template-columns: 1fr !important;
}
}
:root { --drawer-z: 70; --bubble-z: 34; }
[hidden], .advancedToolGroup[hidden], .advancedOnly[hidden], #particleDirectionWrap[hidden] { display: none !important; }
.studioDrawer { z-index: var(--drawer-z) !important; }
.selectionBubble { z-index: var(--bubble-z) !important; }
.studioDrawer:not(.advancedTools) .advancedPanel { display: none !important; }
.tool {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
gap: 6px !important;
border: 2px solid var(--line, #243044) !important;
color: var(--line, #243044) !important;
background: var(--toolBg, #f7f4ea) !important;
box-shadow: 2px 2px 0 rgba(36,48,68,.14) !important;
}
.tool:where(:not(:disabled)):hover { transform: translate(-1px, -1px); box-shadow: 3px 3px 0 rgba(36,48,68,.16) !important; }
.tool.active { color: #fffdf5 !important; background: var(--toolActiveBg, var(--toolAccent, #243044)) !important; }
.toolIcon, .toolPixelIcon, .tool .pixelIcon { display:inline-block; width:14px; height:14px; flex:0 0 14px; image-rendering:pixelated; }
.toolBrush { --toolBg:#dcfce7; --toolAccent:#16a34a; --toolActiveBg:#15803d; }
.toolErase { --toolBg:#fee2e2; --toolAccent:#ef4444; --toolActiveBg:#dc2626; }
.toolFill { --toolBg:#fef3c7; --toolAccent:#f59e0b; --toolActiveBg:#d97706; }
.toolPick { --toolBg:#e0f2fe; --toolAccent:#0284c7; --toolActiveBg:#0369a1; }
.toolAdvanced { --toolBg:#ede9fe; --toolAccent:#7c3aed; --toolActiveBg:#6d28d9; }
.toolUndo, .toolRedo { --toolBg:#f3f4f6; --toolAccent:#64748b; --toolActiveBg:#475569; }
.toolLine, .toolDepth { --toolBg:#dbeafe; --toolAccent:#2563eb; --toolActiveBg:#1d4ed8; }
.toolRect, .toolParticle { --toolBg:#ccfbf1; --toolAccent:#0d9488; --toolActiveBg:#0f766e; }
.toolSelect { --toolBg:#fae8ff; --toolAccent:#c026d3; --toolActiveBg:#a21caf; }
.toolDoor { --toolBg:#ffedd5; --toolAccent:#c2410c; --toolActiveBg:#9a3412; }
.toolOutline { --toolBg:#e0e7ff; --toolAccent:#4f46e5; --toolActiveBg:#4338ca; }
.toolFlipH, .toolFlipV { --toolBg:#fce7f3; --toolAccent:#db2777; --toolActiveBg:#be185d; }
.toolClearSel { --toolBg:#f1f5f9; --toolAccent:#475569; --toolActiveBg:#334155; }
.toolLight { --toolBg:#fef9c3; --toolAccent:#ca8a04; --toolActiveBg:#a16207; }
.tool.danger, #clearPaint { --toolBg:#ffe4e6; --toolAccent:#e11d48; --toolActiveBg:#be123c; }
#toolPick.toolPick, #redoPaint.toolRedo { border-right: 2px solid var(--line, #243044) !important; }
@media (min-width: 761px) {
.studioDrawer.advancedTools { width: clamp(560px, 48vw, 720px) !important; max-width: calc(100vw - 20px) !important; }
.studioDrawer.advancedTools .paintLayout { grid-template-columns: minmax(0, 1fr) clamp(220px, 18vw, 270px) !important; gap: 10px !important; }
.studioDrawer.advancedTools .paintMainColumn #paintCanvas { max-height: min(58vh, 560px) !important; max-width: none !important; justify-self: stretch !important; }
.studioDrawer.advancedTools .palettePanel { width: min(100%, 620px) !important; padding: 6px !important; justify-self: stretch !important; }
.studioDrawer.advancedTools .paletteGrid { grid-template-columns: repeat(14, minmax(16px, 1fr)) !important; gap: 3px !important; }
.studioDrawer.advancedTools .drawControlPanel { display:grid !important; grid-template-columns: minmax(76px, 90px) minmax(120px, 1fr) !important; grid-template-rows:auto auto !important; gap:7px 8px !important; }
.studioDrawer.advancedTools .basicEditorTools { grid-column:1 !important; grid-row:1 !important; }
.studioDrawer.advancedTools .editorUtilityRow { grid-column:1 !important; grid-row:2 !important; }
.studioDrawer.advancedTools .advancedPanel { grid-column:2 !important; grid-row:1 / span 2 !important; display:grid !important; padding:7px !important; overflow:visible !important; }
.studioDrawer.advancedTools .advancedDrawRow,
.studioDrawer.advancedTools .advancedActionRow,
.studioDrawer.advancedTools .extraAdvancedRow { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; gap:5px !important; }
.studioDrawer.advancedTools .advancedPanel :is(.tool, select, .particleControl) { min-height:30px !important; padding:5px !important; font-size:10px !important; }
.studioDrawer.advancedTools .particleControlGroup { grid-column:1 / -1 !important; }
}
.collectionToolbar { display:flex !important; align-items:center !important; gap:6px !important; flex-wrap:nowrap !important; padding:7px !important; overflow:hidden !important; }
.collectionSummary { flex:0 0 auto !important; white-space:nowrap !important; font-size:11px !important; }
.collectionSearch { flex:0 0 108px !important; width:108px !important; min-width:92px !important; min-height:26px !important; padding:3px 6px !important; font-size:10px !important; }
.collectionFilters { flex:1 1 auto !important; min-width:0 !important; display:flex !important; flex-wrap:nowrap !important; gap:5px !important; overflow-x:auto !important; overflow-y:hidden !important; scrollbar-width:thin; }
.collectionFilter { flex:0 0 auto !important; padding:4px 7px !important; font-size:10px !important; white-space:nowrap !important; }
@media (max-width: 760px) {
.collectionToolbar { align-items:stretch !important; overflow-x:auto !important; }
.collectionSummary { display:none !important; }
.collectionSearch { flex-basis:104px !important; width:104px !important; }
.studioDrawer { z-index: var(--drawer-z) !important; }
}