clawmate 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,33 @@
1
+ const os = require('os');
2
+ const path = require('path');
3
+ const { execSync } = require('child_process');
4
+
5
+ const platform = os.platform();
6
+
7
+ function getDesktopPath() {
8
+ if (platform === 'win32') {
9
+ try {
10
+ const result = execSync(
11
+ 'powershell -Command "[Environment]::GetFolderPath(\'Desktop\')"',
12
+ { encoding: 'utf-8' }
13
+ ).trim();
14
+ if (result) return result;
15
+ } catch {}
16
+ return path.join(os.homedir(), 'Desktop');
17
+ }
18
+ return path.join(os.homedir(), 'Desktop');
19
+ }
20
+
21
+ function getTrayIconExt() {
22
+ return platform === 'win32' ? '.ico' : '.png';
23
+ }
24
+
25
+ function isWindows() {
26
+ return platform === 'win32';
27
+ }
28
+
29
+ function isMac() {
30
+ return platform === 'darwin';
31
+ }
32
+
33
+ module.exports = { getDesktopPath, getTrayIconExt, isWindows, isMac, platform };
package/main/store.js ADDED
@@ -0,0 +1,45 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { app } = require('electron');
4
+
5
+ class Store {
6
+ constructor(name, defaults = {}) {
7
+ const userDataPath = app.getPath('userData');
8
+ this.path = path.join(userDataPath, `${name}.json`);
9
+ this.data = { ...defaults };
10
+ this._load();
11
+ }
12
+
13
+ _load() {
14
+ try {
15
+ const raw = fs.readFileSync(this.path, 'utf-8');
16
+ this.data = { ...this.data, ...JSON.parse(raw) };
17
+ } catch {
18
+ // 파일 없으면 기본값 사용
19
+ }
20
+ }
21
+
22
+ _save() {
23
+ try {
24
+ fs.mkdirSync(path.dirname(this.path), { recursive: true });
25
+ fs.writeFileSync(this.path, JSON.stringify(this.data, null, 2));
26
+ } catch (err) {
27
+ console.error('Store save error:', err);
28
+ }
29
+ }
30
+
31
+ get(key) {
32
+ return this.data[key];
33
+ }
34
+
35
+ set(key, value) {
36
+ this.data[key] = value;
37
+ this._save();
38
+ }
39
+
40
+ getAll() {
41
+ return { ...this.data };
42
+ }
43
+ }
44
+
45
+ module.exports = Store;
package/main/tray.js ADDED
@@ -0,0 +1,125 @@
1
+ const { Tray, Menu, nativeImage, app } = require('electron');
2
+ const path = require('path');
3
+ const Store = require('./store');
4
+ const { undoAllMoves, getFileManifest } = require('./file-ops');
5
+ const { isAutoStartEnabled, toggleAutoStart } = require('./autostart');
6
+
7
+ let tray = null;
8
+ let aiBridge = null;
9
+
10
+ function setupTray(mainWindow, bridge) {
11
+ aiBridge = bridge;
12
+ const store = new Store('clawmate-config', { mode: 'pet' });
13
+
14
+ // 트레이 아이콘 생성
15
+ let icon;
16
+ try {
17
+ const iconPath = path.join(__dirname, '..', 'assets', 'icons', 'tray-pet.png');
18
+ icon = nativeImage.createFromPath(iconPath);
19
+ if (icon.isEmpty()) throw new Error('no icon');
20
+ } catch {
21
+ // 16x16 빨간색 아이콘 폴백
22
+ const size = 16;
23
+ const buffer = Buffer.alloc(size * size * 4);
24
+ for (let i = 0; i < size * size; i++) {
25
+ buffer[i * 4 + 0] = 0xff;
26
+ buffer[i * 4 + 1] = 0x4f;
27
+ buffer[i * 4 + 2] = 0x40;
28
+ buffer[i * 4 + 3] = 0xff;
29
+ }
30
+ icon = nativeImage.createFromBuffer(buffer, { width: size, height: size });
31
+ }
32
+
33
+ tray = new Tray(icon);
34
+ tray.setToolTip('ClawMate - 데스크톱 펫');
35
+
36
+ function buildMenu() {
37
+ const mode = store.get('mode') || 'pet';
38
+ const fileInteraction = store.get('fileInteraction') !== false;
39
+ const aiConnected = aiBridge ? aiBridge.isConnected() : false;
40
+ const autoStart = isAutoStartEnabled();
41
+
42
+ return Menu.buildFromTemplate([
43
+ {
44
+ label: `ClawMate (${mode === 'pet' ? 'Clawby' : 'OpenClaw'})`,
45
+ enabled: false,
46
+ },
47
+ {
48
+ label: aiConnected ? 'AI: 연결됨' : 'AI: 자율 모드 (대기 중)',
49
+ enabled: false,
50
+ },
51
+ { type: 'separator' },
52
+ {
53
+ label: 'Pet 모드 (Clawby)',
54
+ type: 'radio',
55
+ checked: mode === 'pet',
56
+ click: () => {
57
+ store.set('mode', 'pet');
58
+ if (mainWindow) mainWindow.webContents.send('mode-changed', 'pet');
59
+ buildAndSet();
60
+ },
61
+ },
62
+ {
63
+ label: 'Incarnation 모드 (OpenClaw)',
64
+ type: 'radio',
65
+ checked: mode === 'incarnation',
66
+ click: () => {
67
+ store.set('mode', 'incarnation');
68
+ if (mainWindow) mainWindow.webContents.send('mode-changed', 'incarnation');
69
+ buildAndSet();
70
+ },
71
+ },
72
+ { type: 'separator' },
73
+ {
74
+ label: '파일 상호작용',
75
+ type: 'checkbox',
76
+ checked: fileInteraction,
77
+ click: (item) => {
78
+ store.set('fileInteraction', item.checked);
79
+ if (mainWindow) mainWindow.webContents.send('config-changed', store.getAll());
80
+ },
81
+ },
82
+ {
83
+ label: '컴퓨터 시작 시 자동 실행',
84
+ type: 'checkbox',
85
+ checked: autoStart,
86
+ click: () => {
87
+ toggleAutoStart();
88
+ buildAndSet();
89
+ },
90
+ },
91
+ { type: 'separator' },
92
+ {
93
+ label: '파일 이동 되돌리기',
94
+ click: async () => {
95
+ const manifest = await getFileManifest();
96
+ const pending = manifest.filter(m => !m.restored);
97
+ if (pending.length === 0) return;
98
+ await undoAllMoves();
99
+ },
100
+ },
101
+ { type: 'separator' },
102
+ {
103
+ label: '종료',
104
+ click: () => {
105
+ app.quit();
106
+ },
107
+ },
108
+ ]);
109
+ }
110
+
111
+ function buildAndSet() {
112
+ tray.setContextMenu(buildMenu());
113
+ }
114
+
115
+ // AI 연결 상태 변경 시 메뉴 업데이트
116
+ if (aiBridge) {
117
+ aiBridge.on('connected', () => buildAndSet());
118
+ aiBridge.on('disconnected', () => buildAndSet());
119
+ }
120
+
121
+ buildAndSet();
122
+ return tray;
123
+ }
124
+
125
+ module.exports = { setupTray };
@@ -0,0 +1,15 @@
1
+ {
2
+ "id": "clawmate",
3
+ "name": "ClawMate",
4
+ "description": "OpenClaw 데스크톱 펫 - 화면 위의 살아있는 Claw",
5
+ "version": "1.0.0",
6
+ "skills": ["skills/launch-pet"],
7
+ "configSchema": {
8
+ "type": "object",
9
+ "properties": {
10
+ "mode": { "enum": ["pet", "incarnation"], "default": "pet" },
11
+ "fileInteraction": { "type": "boolean", "default": true },
12
+ "soundEnabled": { "type": "boolean", "default": false }
13
+ }
14
+ }
15
+ }
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "clawmate",
3
+ "version": "1.0.0",
4
+ "description": "OpenClaw 데스크톱 펫 - AI가 조종하는 화면 위의 살아있는 Claw",
5
+ "main": "main/index.js",
6
+ "bin": {
7
+ "clawmate": "./skills/launch-pet/index.js"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/boqum/clawmate.git"
12
+ },
13
+ "scripts": {
14
+ "start": "electron .",
15
+ "build": "electron-builder",
16
+ "build:win": "electron-builder --win",
17
+ "build:mac": "electron-builder --mac"
18
+ },
19
+ "keywords": ["openclaw", "desktop-pet", "electron", "clawmate", "ai-pet"],
20
+ "author": "OpenClaw",
21
+ "license": "MIT",
22
+ "homepage": "https://github.com/boqum/clawmate",
23
+ "dependencies": {
24
+ "ws": "^8.18.0"
25
+ },
26
+ "devDependencies": {
27
+ "electron": "^33.0.0",
28
+ "electron-builder": "^25.0.0"
29
+ }
30
+ }
@@ -0,0 +1,57 @@
1
+ const { contextBridge, ipcRenderer } = require('electron');
2
+
3
+ contextBridge.exposeInMainWorld('clawmate', {
4
+ // 클릭 통과 제어
5
+ setClickThrough: (ignore) => ipcRenderer.send('set-click-through', ignore),
6
+
7
+ // 파일 작업
8
+ getDesktopFiles: () => ipcRenderer.invoke('get-desktop-files'),
9
+ moveFile: (fileName, newPosition) => ipcRenderer.invoke('move-file', fileName, newPosition),
10
+ undoFileMove: (moveId) => ipcRenderer.invoke('undo-file-move', moveId),
11
+ undoAllMoves: () => ipcRenderer.invoke('undo-all-moves'),
12
+ getFileManifest: () => ipcRenderer.invoke('get-file-manifest'),
13
+
14
+ // 모드 전환
15
+ getMode: () => ipcRenderer.invoke('get-mode'),
16
+ setMode: (mode) => ipcRenderer.invoke('set-mode', mode),
17
+
18
+ // 설정
19
+ getConfig: () => ipcRenderer.invoke('get-config'),
20
+ setConfig: (key, value) => ipcRenderer.invoke('set-config', key, value),
21
+
22
+ // 메모리 (사용자 상호작용 기억)
23
+ getMemory: () => ipcRenderer.invoke('get-memory'),
24
+ saveMemory: (data) => ipcRenderer.invoke('save-memory', data),
25
+
26
+ // 창 정보
27
+ getScreenSize: () => ipcRenderer.invoke('get-screen-size'),
28
+
29
+ // 이벤트 수신
30
+ onModeChanged: (callback) => {
31
+ ipcRenderer.on('mode-changed', (_, mode) => callback(mode));
32
+ },
33
+ onConfigChanged: (callback) => {
34
+ ipcRenderer.on('config-changed', (_, config) => callback(config));
35
+ },
36
+
37
+ // === OpenClaw AI 통신 ===
38
+
39
+ // AI 명령 수신 (OpenClaw → 펫)
40
+ onAICommand: (callback) => {
41
+ ipcRenderer.on('ai-command', (_, command) => callback(command));
42
+ },
43
+
44
+ // AI 연결/해제 이벤트
45
+ onAIConnected: (callback) => {
46
+ ipcRenderer.on('ai-connected', () => callback());
47
+ },
48
+ onAIDisconnected: (callback) => {
49
+ ipcRenderer.on('ai-disconnected', () => callback());
50
+ },
51
+
52
+ // 사용자 이벤트를 OpenClaw에 전달 (펫 → OpenClaw)
53
+ reportToAI: (event, data) => ipcRenderer.send('report-to-ai', event, data),
54
+
55
+ // AI 연결 상태 확인
56
+ isAIConnected: () => ipcRenderer.invoke('is-ai-connected'),
57
+ });
@@ -0,0 +1,87 @@
1
+ /* 파티클 및 전환 효과 */
2
+
3
+ /* 모드 전환 파티클 */
4
+ .mode-transition-particle {
5
+ position: absolute;
6
+ width: 4px;
7
+ height: 4px;
8
+ border-radius: 50%;
9
+ animation: particle-burst 0.8s ease-out forwards;
10
+ pointer-events: none;
11
+ }
12
+
13
+ @keyframes particle-burst {
14
+ 0% {
15
+ opacity: 1;
16
+ transform: translate(0, 0) scale(1);
17
+ }
18
+ 100% {
19
+ opacity: 0;
20
+ transform: translate(var(--px), var(--py)) scale(0);
21
+ }
22
+ }
23
+
24
+ /* 진화 글로우 링 */
25
+ .evolve-ring {
26
+ position: absolute;
27
+ border-radius: 50%;
28
+ border: 2px solid;
29
+ animation: ring-expand 1s ease-out forwards;
30
+ pointer-events: none;
31
+ }
32
+
33
+ @keyframes ring-expand {
34
+ 0% {
35
+ opacity: 1;
36
+ transform: scale(0.5);
37
+ }
38
+ 100% {
39
+ opacity: 0;
40
+ transform: scale(3);
41
+ }
42
+ }
43
+
44
+ /* 파일 픽업 이펙트 */
45
+ .file-pickup-effect {
46
+ position: absolute;
47
+ font-size: 12px;
48
+ color: #ff4f40;
49
+ font-weight: bold;
50
+ animation: pickup-float 1s ease-out forwards;
51
+ pointer-events: none;
52
+ user-select: none;
53
+ }
54
+
55
+ @keyframes pickup-float {
56
+ 0% { opacity: 1; transform: translateY(0); }
57
+ 100% { opacity: 0; transform: translateY(-30px); }
58
+ }
59
+
60
+ /* 꼬리 흔들기 (idle 상태 추가 효과) */
61
+ .tail-wag {
62
+ animation: wag 0.5s ease-in-out infinite alternate;
63
+ }
64
+
65
+ @keyframes wag {
66
+ 0% { transform: rotate(-5deg); }
67
+ 100% { transform: rotate(5deg); }
68
+ }
69
+
70
+ /* 진화 단계 전환 — 밝은 플래시 */
71
+ .evolve-flash {
72
+ position: fixed;
73
+ top: 0;
74
+ left: 0;
75
+ width: 100%;
76
+ height: 100%;
77
+ background: white;
78
+ animation: flash-in-out 0.6s ease forwards;
79
+ pointer-events: none;
80
+ z-index: 9999;
81
+ }
82
+
83
+ @keyframes flash-in-out {
84
+ 0% { opacity: 0; }
85
+ 30% { opacity: 0.3; }
86
+ 100% { opacity: 0; }
87
+ }
@@ -0,0 +1,127 @@
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ body {
8
+ font-family: 'Segoe UI', 'Apple SD Gothic Neo', sans-serif;
9
+ background: transparent;
10
+ overflow: hidden;
11
+ user-select: none;
12
+ }
13
+
14
+ .launcher {
15
+ width: 360px;
16
+ padding: 24px;
17
+ background: #ffffff;
18
+ border-radius: 16px;
19
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
20
+ }
21
+
22
+ .launcher-header {
23
+ text-align: center;
24
+ margin-bottom: 20px;
25
+ }
26
+
27
+ .logo {
28
+ font-size: 36px;
29
+ margin-bottom: 8px;
30
+ }
31
+
32
+ .launcher-header h1 {
33
+ font-size: 22px;
34
+ color: #1a1a2e;
35
+ font-weight: 700;
36
+ }
37
+
38
+ .subtitle {
39
+ color: #888;
40
+ font-size: 13px;
41
+ margin-top: 4px;
42
+ }
43
+
44
+ .launcher-body {
45
+ display: flex;
46
+ gap: 12px;
47
+ margin-bottom: 20px;
48
+ }
49
+
50
+ .mode-card {
51
+ flex: 1;
52
+ padding: 16px 12px;
53
+ border: 2px solid #e0e0e0;
54
+ border-radius: 12px;
55
+ text-align: center;
56
+ cursor: pointer;
57
+ transition: all 0.2s ease;
58
+ }
59
+
60
+ .mode-card:hover {
61
+ border-color: #ccc;
62
+ background: #fafafa;
63
+ }
64
+
65
+ .mode-card.selected {
66
+ border-color: #ff4f40;
67
+ background: #fff5f5;
68
+ box-shadow: 0 0 0 3px rgba(255, 79, 64, 0.15);
69
+ }
70
+
71
+ #mode-incarnation.selected {
72
+ border-color: #00BFA5;
73
+ background: #f0fffd;
74
+ box-shadow: 0 0 0 3px rgba(0, 191, 165, 0.15);
75
+ }
76
+
77
+ .mode-icon {
78
+ font-size: 32px;
79
+ margin-bottom: 8px;
80
+ }
81
+
82
+ .mode-card h2 {
83
+ font-size: 15px;
84
+ color: #1a1a2e;
85
+ margin-bottom: 2px;
86
+ }
87
+
88
+ .mode-card p {
89
+ font-size: 11px;
90
+ color: #888;
91
+ margin-bottom: 6px;
92
+ }
93
+
94
+ .mode-desc {
95
+ font-size: 11px;
96
+ color: #666;
97
+ line-height: 1.4;
98
+ }
99
+
100
+ .start-btn {
101
+ width: 100%;
102
+ padding: 12px;
103
+ border: none;
104
+ border-radius: 10px;
105
+ background: #ff4f40;
106
+ color: white;
107
+ font-size: 15px;
108
+ font-weight: 600;
109
+ cursor: pointer;
110
+ transition: all 0.2s ease;
111
+ }
112
+
113
+ .start-btn:disabled {
114
+ background: #ddd;
115
+ color: #999;
116
+ cursor: not-allowed;
117
+ }
118
+
119
+ .start-btn:not(:disabled):hover {
120
+ background: #e63e30;
121
+ transform: translateY(-1px);
122
+ box-shadow: 0 4px 12px rgba(255, 79, 64, 0.3);
123
+ }
124
+
125
+ .start-btn:not(:disabled):active {
126
+ transform: translateY(0);
127
+ }
@@ -0,0 +1,109 @@
1
+ /* 펫 컨테이너 */
2
+ #pet-container {
3
+ position: absolute;
4
+ width: 64px;
5
+ height: 64px;
6
+ transition: transform 0.1s ease;
7
+ image-rendering: pixelated;
8
+ z-index: 1000;
9
+ }
10
+
11
+ #pet-container canvas {
12
+ display: block;
13
+ image-rendering: pixelated;
14
+ image-rendering: crisp-edges;
15
+ }
16
+
17
+ /* 수면 Z 이펙트 */
18
+ .sleep-z {
19
+ position: absolute;
20
+ top: -20px;
21
+ right: -10px;
22
+ font-size: 14px;
23
+ color: #888;
24
+ animation: float-z 2s ease-in-out infinite;
25
+ pointer-events: none;
26
+ user-select: none;
27
+ }
28
+
29
+ .sleep-z:nth-child(2) {
30
+ top: -35px;
31
+ right: -5px;
32
+ font-size: 18px;
33
+ animation-delay: 0.5s;
34
+ }
35
+
36
+ .sleep-z:nth-child(3) {
37
+ top: -50px;
38
+ right: 0px;
39
+ font-size: 22px;
40
+ animation-delay: 1s;
41
+ }
42
+
43
+ @keyframes float-z {
44
+ 0% { opacity: 0; transform: translateY(0) translateX(0); }
45
+ 30% { opacity: 1; }
46
+ 100% { opacity: 0; transform: translateY(-20px) translateX(10px); }
47
+ }
48
+
49
+ /* 흥분 별 이펙트 */
50
+ .star-effect {
51
+ position: absolute;
52
+ width: 8px;
53
+ height: 8px;
54
+ background: #FFD700;
55
+ clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
56
+ animation: star-pop 0.6s ease-out forwards;
57
+ pointer-events: none;
58
+ }
59
+
60
+ @keyframes star-pop {
61
+ 0% { opacity: 1; transform: scale(0); }
62
+ 50% { opacity: 1; transform: scale(1.5); }
63
+ 100% { opacity: 0; transform: scale(0.5) translateY(-20px); }
64
+ }
65
+
66
+ /* 하트 이펙트 (상호작용) */
67
+ .heart-effect {
68
+ position: absolute;
69
+ font-size: 16px;
70
+ animation: heart-float 1s ease-out forwards;
71
+ pointer-events: none;
72
+ user-select: none;
73
+ }
74
+
75
+ @keyframes heart-float {
76
+ 0% { opacity: 1; transform: translateY(0) scale(0.5); }
77
+ 100% { opacity: 0; transform: translateY(-40px) scale(1.2); }
78
+ }
79
+
80
+ /* 진화 반짝임 이펙트 */
81
+ .evolve-sparkle {
82
+ position: absolute;
83
+ width: 6px;
84
+ height: 6px;
85
+ border-radius: 50%;
86
+ animation: sparkle-pop 0.8s ease-out forwards;
87
+ pointer-events: none;
88
+ }
89
+
90
+ @keyframes sparkle-pop {
91
+ 0% { opacity: 1; transform: scale(0) rotate(0deg); }
92
+ 50% { opacity: 1; transform: scale(1.5) rotate(180deg); }
93
+ 100% { opacity: 0; transform: scale(0) rotate(360deg); }
94
+ }
95
+
96
+ /* 드래그 중 상태 */
97
+ #pet-container.dragging {
98
+ cursor: grabbing;
99
+ filter: brightness(1.1);
100
+ }
101
+
102
+ /* 모드별 글로우 효과 */
103
+ #pet-container.mode-incarnation canvas {
104
+ filter: drop-shadow(0 0 4px rgba(0, 191, 165, 0.6));
105
+ }
106
+
107
+ #pet-container.mode-pet canvas {
108
+ filter: drop-shadow(0 0 2px rgba(255, 79, 64, 0.3));
109
+ }
@@ -0,0 +1,72 @@
1
+ /* 말풍선 공통 */
2
+ .speech-bubble {
3
+ position: absolute;
4
+ padding: 8px 14px;
5
+ font-family: 'Segoe UI', 'Apple SD Gothic Neo', sans-serif;
6
+ font-size: 13px;
7
+ line-height: 1.4;
8
+ white-space: nowrap;
9
+ pointer-events: none;
10
+ z-index: 2000;
11
+ animation: speech-appear 0.2s ease-out;
12
+ max-width: 220px;
13
+ white-space: normal;
14
+ word-break: keep-all;
15
+ }
16
+
17
+ .speech-bubble::after {
18
+ content: '';
19
+ position: absolute;
20
+ bottom: -8px;
21
+ left: 30px;
22
+ width: 0;
23
+ height: 0;
24
+ border-left: 8px solid transparent;
25
+ border-right: 8px solid transparent;
26
+ }
27
+
28
+ /* Pet 모드 말풍선 — 둥글고 빨간 테두리 */
29
+ .speech-pet {
30
+ background: #fff5f5;
31
+ border: 2px solid #ff4f40;
32
+ border-radius: 16px;
33
+ color: #333;
34
+ box-shadow: 0 2px 8px rgba(255, 79, 64, 0.15);
35
+ }
36
+
37
+ .speech-pet::after {
38
+ border-top: 8px solid #ff4f40;
39
+ }
40
+
41
+ /* Incarnation 모드 말풍선 — 각지고 틸 발광 */
42
+ .speech-incarnation {
43
+ background: #f0fffd;
44
+ border: 2px solid #00BFA5;
45
+ border-radius: 4px;
46
+ color: #1a1a2e;
47
+ box-shadow: 0 0 12px rgba(0, 191, 165, 0.3);
48
+ }
49
+
50
+ .speech-incarnation::after {
51
+ border-top: 8px solid #00BFA5;
52
+ }
53
+
54
+ /* 타자기 커서 효과 */
55
+ .speech-text {
56
+ display: inline;
57
+ }
58
+
59
+ /* 페이드 아웃 */
60
+ .speech-fade {
61
+ animation: speech-disappear 0.5s ease-in forwards;
62
+ }
63
+
64
+ @keyframes speech-appear {
65
+ 0% { opacity: 0; transform: translateY(5px) scale(0.9); }
66
+ 100% { opacity: 1; transform: translateY(0) scale(1); }
67
+ }
68
+
69
+ @keyframes speech-disappear {
70
+ 0% { opacity: 1; transform: scale(1); }
71
+ 100% { opacity: 0; transform: scale(0.9) translateY(-5px); }
72
+ }