webui: migrate mobile components to SketchTailwindElement

Complete migration of all mobile web components from LitElement to
SketchTailwindElement base class with Tailwind CSS styling:

Components migrated:
- mobile-chat-input.ts: Chat input with file upload, textarea auto-resize
- mobile-chat.ts: Message display with markdown rendering and tool calls
- mobile-diff.ts: Git diff viewer with Monaco editor integration
- mobile-shell.ts: Main container coordinating mobile UI layout
- mobile-title.ts: Header with connection status and view switching

Key changes:
- Replaced LitElement inheritance with SketchTailwindElement
- Converted all CSS-in-JS styles to Tailwind utility classes
- Removed static styles blocks and shadow DOM styling
- Added custom animations via document.head for non-Tailwind effects
- Preserved all existing functionality and component interactions

Technical improvements:
- Consistent iOS safe area support with env() CSS functions
- Proper flexbox layouts for mobile responsive design
- Maintained accessibility with proper ARIA labels and focus states
- Enhanced hover and active states using Tailwind modifiers
- Optimized touch interactions with -webkit-overflow-scrolling

The mobile components now follow the established SketchTailwindElement
pattern while maintaining full feature parity with the original
shadow DOM implementations.

Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: s21f840091392b02ek
diff --git a/webui/src/web-components/mobile-title.ts b/webui/src/web-components/mobile-title.ts
index 8ac9166..92bde10 100644
--- a/webui/src/web-components/mobile-title.ts
+++ b/webui/src/web-components/mobile-title.ts
@@ -1,9 +1,10 @@
-import { css, html, LitElement } from "lit";
+import { html } from "lit";
 import { customElement, property } from "lit/decorators.js";
 import { ConnectionStatus } from "../data";
+import { SketchTailwindElement } from "./sketch-tailwind-element";
 
 @customElement("mobile-title")
-export class MobileTitle extends LitElement {
+export class MobileTitle extends SketchTailwindElement {
   @property({ type: String })
   connectionStatus: ConnectionStatus = "disconnected";
 
@@ -19,161 +20,30 @@
   @property({ type: String })
   slug: string = "";
 
-  static styles = css`
-    :host {
-      display: block;
-      background-color: #f8f9fa;
-      border-bottom: 1px solid #e9ecef;
-      padding: 12px 16px;
+  connectedCallback() {
+    super.connectedCallback();
+    // Add animation styles to document head if not already present
+    if (!document.getElementById("mobile-title-animations")) {
+      const style = document.createElement("style");
+      style.id = "mobile-title-animations";
+      style.textContent = `
+        @keyframes pulse {
+          0%, 100% { opacity: 1; }
+          50% { opacity: 0.5; }
+        }
+        @keyframes thinking {
+          0%, 80%, 100% { transform: scale(0); }
+          40% { transform: scale(1); }
+        }
+        .pulse-animation { animation: pulse 1.5s ease-in-out infinite; }
+        .thinking-animation { animation: thinking 1.4s ease-in-out infinite both; }
+        .thinking-animation:nth-child(1) { animation-delay: -0.32s; }
+        .thinking-animation:nth-child(2) { animation-delay: -0.16s; }
+        .thinking-animation:nth-child(3) { animation-delay: 0; }
+      `;
+      document.head.appendChild(style);
     }
-
-    .title-container {
-      display: flex;
-      align-items: flex-start;
-      justify-content: space-between;
-    }
-
-    .title-section {
-      flex: 1;
-      min-width: 0;
-    }
-
-    .right-section {
-      display: flex;
-      align-items: center;
-      gap: 12px;
-    }
-
-    .view-selector {
-      background: none;
-      border: 1px solid #e9ecef;
-      border-radius: 6px;
-      padding: 6px 8px;
-      font-size: 14px;
-      font-weight: 500;
-      cursor: pointer;
-      transition: all 0.2s ease;
-      color: #495057;
-      min-width: 60px;
-    }
-
-    .view-selector:hover {
-      background-color: #f8f9fa;
-      border-color: #dee2e6;
-    }
-
-    .view-selector:focus {
-      outline: none;
-      border-color: #007acc;
-      box-shadow: 0 0 0 2px rgba(0, 122, 204, 0.2);
-    }
-
-    .title {
-      font-size: 18px;
-      font-weight: 600;
-      color: #212529;
-      margin: 0;
-    }
-
-    .title a {
-      color: inherit;
-      text-decoration: none;
-      transition: opacity 0.2s ease;
-      display: flex;
-      align-items: center;
-      gap: 8px;
-    }
-
-    .title a:hover {
-      opacity: 0.8;
-      text-decoration: underline;
-    }
-
-    .title img {
-      width: 18px;
-      height: 18px;
-      border-radius: 3px;
-    }
-
-    .status-indicator {
-      display: flex;
-      align-items: center;
-      gap: 8px;
-      font-size: 14px;
-    }
-
-    .status-dot {
-      width: 8px;
-      height: 8px;
-      border-radius: 50%;
-      flex-shrink: 0;
-    }
-
-    .status-dot.connected {
-      background-color: #28a745;
-    }
-
-    .status-dot.connecting {
-      background-color: #ffc107;
-      animation: pulse 1.5s ease-in-out infinite;
-    }
-
-    .status-dot.disconnected {
-      background-color: #dc3545;
-    }
-
-    .thinking-indicator {
-      display: flex;
-      align-items: center;
-      gap: 6px;
-      color: #6c757d;
-      font-size: 13px;
-    }
-
-    .thinking-dots {
-      display: flex;
-      gap: 2px;
-    }
-
-    .thinking-dot {
-      width: 4px;
-      height: 4px;
-      border-radius: 50%;
-      background-color: #6c757d;
-      animation: thinking 1.4s ease-in-out infinite both;
-    }
-
-    .thinking-dot:nth-child(1) {
-      animation-delay: -0.32s;
-    }
-    .thinking-dot:nth-child(2) {
-      animation-delay: -0.16s;
-    }
-    .thinking-dot:nth-child(3) {
-      animation-delay: 0;
-    }
-
-    @keyframes pulse {
-      0%,
-      100% {
-        opacity: 1;
-      }
-      50% {
-        opacity: 0.5;
-      }
-    }
-
-    @keyframes thinking {
-      0%,
-      80%,
-      100% {
-        transform: scale(0);
-      }
-      40% {
-        transform: scale(1);
-      }
-    }
-  `;
+  }
 
   private getStatusText() {
     switch (this.connectionStatus) {
@@ -202,45 +72,67 @@
   }
 
   render() {
+    const statusDotClass =
+      {
+        connected: "bg-green-500",
+        connecting: "bg-yellow-500 pulse-animation",
+        disconnected: "bg-red-500",
+      }[this.connectionStatus] || "bg-gray-500";
+
     return html`
-      <div class="title-container">
-        <div class="title-section">
-          <h1 class="title">
-            ${this.skabandAddr
-              ? html`<a
-                  href="${this.skabandAddr}"
-                  target="_blank"
-                  rel="noopener noreferrer"
-                >
-                  <img src="${this.skabandAddr}/sketch.dev.png" alt="sketch" />
-                  ${this.slug || "Sketch"}
-                </a>`
-              : html`${this.slug || "Sketch"}`}
-          </h1>
-        </div>
+      <div class="block bg-gray-50 border-b border-gray-200 p-3">
+        <div class="flex items-start justify-between">
+          <div class="flex-1 min-w-0">
+            <h1 class="text-lg font-semibold text-gray-900 m-0">
+              ${this.skabandAddr
+                ? html`<a
+                    href="${this.skabandAddr}"
+                    target="_blank"
+                    rel="noopener noreferrer"
+                    class="text-inherit no-underline transition-opacity duration-200 flex items-center gap-2 hover:opacity-80 hover:underline"
+                  >
+                    <img
+                      src="${this.skabandAddr}/sketch.dev.png"
+                      alt="sketch"
+                      class="w-[18px] h-[18px] rounded"
+                    />
+                    ${this.slug || "Sketch"}
+                  </a>`
+                : html`${this.slug || "Sketch"}`}
+            </h1>
+          </div>
 
-        <div class="right-section">
-          <select
-            class="view-selector"
-            .value=${this.currentView}
-            @change=${this.handleViewChange}
-          >
-            <option value="chat">Chat</option>
-            <option value="diff">Diff</option>
-          </select>
+          <div class="flex items-center gap-3">
+            <select
+              class="bg-transparent border border-gray-200 rounded px-2 py-1.5 text-sm font-medium cursor-pointer transition-all duration-200 text-gray-700 min-w-[60px] hover:bg-gray-50 hover:border-gray-300 focus:outline-none focus:border-blue-500 focus:shadow-sm focus:ring-2 focus:ring-blue-200"
+              .value=${this.currentView}
+              @change=${this.handleViewChange}
+            >
+              <option value="chat">Chat</option>
+              <option value="diff">Diff</option>
+            </select>
 
-          ${this.isThinking
-            ? html`
-                <div class="thinking-indicator">
-                  <span>thinking</span>
-                  <div class="thinking-dots">
-                    <div class="thinking-dot"></div>
-                    <div class="thinking-dot"></div>
-                    <div class="thinking-dot"></div>
+            ${this.isThinking
+              ? html`
+                  <div class="flex items-center gap-1.5 text-gray-500 text-xs">
+                    <span>thinking</span>
+                    <div class="flex gap-0.5">
+                      <div
+                        class="w-1 h-1 rounded-full bg-gray-500 thinking-animation"
+                      ></div>
+                      <div
+                        class="w-1 h-1 rounded-full bg-gray-500 thinking-animation"
+                      ></div>
+                      <div
+                        class="w-1 h-1 rounded-full bg-gray-500 thinking-animation"
+                      ></div>
+                    </div>
                   </div>
-                </div>
-              `
-            : html` <span class="status-dot ${this.connectionStatus}"></span> `}
+                `
+              : html`<span
+                  class="w-2 h-2 rounded-full flex-shrink-0 ${statusDotClass}"
+                ></span>`}
+          </div>
         </div>
       </div>
     `;