]> danix's work - danix.xyz-2.git/commitdiff
feat: add form component styles (inputs, textarea, checkbox, radio)
authorDanilo M. <redacted>
Thu, 16 Apr 2026 14:53:21 +0000 (16:53 +0200)
committerDanilo M. <redacted>
Thu, 16 Apr 2026 14:53:21 +0000 (16:53 +0200)
themes/danix-xyz-hacker/assets/css/main.css
themes/danix-xyz-hacker/assets/css/main.min.css

index 9e5c86a8d5118adb92e2c33fd73a1859b6439ec4..19584bf19b429c06d593e7247516b6bdc8688cf8 100644 (file)
@@ -595,3 +595,477 @@ article.border.border-border\/30.rounded-lg.overflow-hidden.group.bg-bg {
   border-color: var(--border);
   box-shadow: 0 0 20px var(--accent-glow);
 }
+
+/* ============================================
+   FORM COMPONENTS (Week 4)
+   ============================================ */
+
+/* Form input base styles */
+.form-input,
+.form-textarea,
+.form-select {
+  @apply w-full px-4 py-2 rounded border border-border bg-bg2 text-text font-body transition-all duration-200;
+}
+
+/* Input placeholder styling */
+.form-input::placeholder,
+.form-textarea::placeholder {
+  color: var(--text-dim);
+  opacity: 0.7;
+}
+
+/* Input focus state */
+.form-input:focus,
+.form-textarea:focus,
+.form-select:focus {
+  @apply outline-none border-accent ring-2 ring-accent ring-offset-2;
+  ring-offset-color: var(--bg);
+  border-color: var(--accent);
+}
+
+/* Input invalid/error state */
+.form-input:invalid,
+.form-textarea:invalid,
+.form-select:invalid,
+.form-input.error,
+.form-textarea.error,
+.form-select.error {
+  @apply border-red-500 ring-red-500;
+}
+
+.form-input:invalid:focus,
+.form-textarea:invalid:focus,
+.form-select:invalid:focus {
+  @apply ring-red-500 border-red-500;
+  ring-offset-color: var(--bg);
+}
+
+/* Input disabled state */
+.form-input:disabled,
+.form-textarea:disabled,
+.form-select:disabled {
+  @apply opacity-50 cursor-not-allowed bg-muted;
+}
+
+/* Textarea specific */
+.form-textarea {
+  @apply min-h-32;
+  resize: vertical;
+}
+
+.form-textarea.auto-expand {
+  resize: none;
+  overflow-y: hidden;
+}
+
+/* Select dropdown */
+.form-select {
+  cursor: pointer;
+  appearance: none;
+  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23a855f7' d='M6 9L1 4h10z'/%3E%3C/svg%3E");
+  background-repeat: no-repeat;
+  background-position: right 0.75rem center;
+  padding-right: 2.5rem;
+}
+
+html.theme-light .form-select {
+  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%239333ea' d='M6 9L1 4h10z'/%3E%3C/svg%3E");
+}
+
+/* Checkbox and radio button base */
+.form-checkbox,
+.form-radio {
+  @apply w-5 h-5 cursor-pointer accent-accent transition-all duration-200;
+  appearance: none;
+  border: 2px solid var(--border);
+  border-radius: 0.375rem;
+  flex-shrink: 0;
+}
+
+.form-radio {
+  border-radius: 50%;
+}
+
+/* Checkbox/radio focus state */
+.form-checkbox:focus-visible,
+.form-radio:focus-visible {
+  @apply outline-none ring-2 ring-accent ring-offset-2;
+  ring-offset-color: var(--bg);
+}
+
+/* Checkbox/radio checked state */
+.form-checkbox:checked,
+.form-radio:checked {
+  background-color: var(--accent);
+  border-color: var(--accent);
+  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3E%3C/svg%3E");
+  background-repeat: no-repeat;
+  background-position: center;
+  background-size: 100%;
+}
+
+.form-radio:checked {
+  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='8' cy='8' r='3.5'/%3E%3C/svg%3E");
+}
+
+/* Checkbox/radio disabled state */
+.form-checkbox:disabled,
+.form-radio:disabled {
+  @apply opacity-50 cursor-not-allowed;
+  border-color: var(--muted);
+}
+
+/* Form group layout */
+.form-group {
+  @apply space-y-2;
+}
+
+.form-group label {
+  @apply block text-sm font-semibold text-text;
+}
+
+.form-group.required label::after {
+  content: ' *';
+  color: #ef4444;
+}
+
+.form-group-input {
+  @apply relative;
+}
+
+.form-group-help {
+  @apply text-xs text-text-dim;
+}
+
+.form-group.error .form-group-help {
+  @apply text-red-500;
+}
+
+.form-error {
+  @apply text-sm text-red-500 mt-1;
+}
+
+/* Form layout utilities */
+.form-row {
+  @apply flex flex-col md:flex-row gap-4;
+}
+
+.form-row > .form-group {
+  @apply flex-1;
+}
+
+.form-inline {
+  @apply flex flex-col sm:flex-row items-end gap-4;
+}
+
+.form-inline .form-group {
+  @apply flex-1;
+}
+
+.form-inline .btn {
+  @apply h-10;
+}
+
+/* Character count indicator */
+.form-char-count {
+  @apply text-xs text-text-dim text-right;
+}
+
+.form-char-count.warning {
+  @apply text-amber-500;
+}
+
+.form-char-count.error {
+  @apply text-red-500;
+}
+
+/* ============================================
+   MODAL COMPONENTS (Week 4)
+   ============================================ */
+
+/* Modal backdrop */
+.modal-backdrop {
+  @apply fixed inset-0 bg-black/50 opacity-0 invisible transition-all duration-300 z-40;
+  backdrop-filter: blur(2px);
+}
+
+.modal-backdrop.active {
+  @apply opacity-100 visible;
+}
+
+/* Modal container */
+.modal {
+  @apply fixed inset-0 flex items-center justify-center opacity-0 invisible transition-all duration-300 z-50 p-4;
+  pointer-events: none;
+}
+
+.modal.active {
+  @apply opacity-100 visible;
+  pointer-events: auto;
+}
+
+.modal.active .modal-backdrop {
+  @apply opacity-100 visible;
+  pointer-events: auto;
+}
+
+/* Modal content box */
+.modal-content {
+  @apply bg-bg2 border border-border rounded-lg shadow-2xl max-w-lg w-full max-h-[90vh] flex flex-col;
+  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
+  animation: modalSlideUp 0.3s ease-out;
+}
+
+@keyframes modalSlideUp {
+  from {
+    transform: translateY(20px);
+    opacity: 0;
+  }
+  to {
+    transform: translateY(0);
+    opacity: 1;
+  }
+}
+
+/* Modal header */
+.modal-header {
+  @apply flex items-start justify-between gap-4 p-6 border-b border-border;
+}
+
+.modal-title {
+  @apply text-xl font-bold text-text;
+}
+
+.modal-close {
+  @apply flex items-center justify-center w-8 h-8 rounded hover:bg-surface cursor-pointer transition-colors;
+}
+
+.modal-close::before,
+.modal-close::after {
+  content: '';
+  @apply absolute w-5 h-0.5 bg-text-dim;
+}
+
+.modal-close::before {
+  transform: rotate(45deg);
+}
+
+.modal-close::after {
+  transform: rotate(-45deg);
+}
+
+.modal-close:hover::before,
+.modal-close:hover::after {
+  @apply bg-accent;
+}
+
+/* Modal body */
+.modal-body {
+  @apply flex-1 overflow-y-auto p-6 space-y-4;
+}
+
+/* Modal footer */
+.modal-footer {
+  @apply flex items-center justify-end gap-3 p-6 border-t border-border;
+}
+
+/* Modal sizes */
+.modal-content.modal-sm {
+  @apply max-w-sm;
+}
+
+.modal-content.modal-md {
+  @apply max-w-md;
+}
+
+.modal-content.modal-lg {
+  @apply max-w-2xl;
+}
+
+/* Modal variants */
+.modal-content.modal-alert {
+  @apply max-w-sm;
+}
+
+.modal-content.modal-confirm {
+  @apply max-w-sm;
+}
+
+/* Modal button styling */
+.modal-footer .btn {
+  @apply min-w-[100px];
+}
+
+/* ============================================
+   INTERACTIVE PATTERNS (Week 4)
+   ============================================ */
+
+/* Loading spinner */
+.spinner {
+  @apply inline-block;
+  width: 1rem;
+  height: 1rem;
+  border: 2px solid var(--border);
+  border-top-color: var(--accent);
+  border-radius: 50%;
+  animation: spin 0.6s linear infinite;
+}
+
+@keyframes spin {
+  to {
+    transform: rotate(360deg);
+  }
+}
+
+.spinner-sm {
+  width: 0.75rem;
+  height: 0.75rem;
+  border-width: 1.5px;
+}
+
+.spinner-lg {
+  width: 1.5rem;
+  height: 1.5rem;
+  border-width: 3px;
+}
+
+/* Button with spinner */
+.btn:disabled .spinner {
+  @apply inline-block mr-2;
+}
+
+/* Toast notification container */
+.toast-container {
+  @apply fixed bottom-0 right-0 p-4 space-y-3 max-w-sm z-50;
+}
+
+@media (max-width: 640px) {
+  .toast-container {
+    @apply left-0 right-0 px-4;
+  }
+}
+
+/* Toast base */
+.toast {
+  @apply flex items-start gap-3 p-4 rounded-lg border border-border shadow-lg;
+  animation: slideInUp 0.3s ease-out;
+  background-color: var(--bg2);
+  color: var(--text);
+}
+
+@keyframes slideInUp {
+  from {
+    transform: translateY(100%);
+    opacity: 0;
+  }
+  to {
+    transform: translateY(0);
+    opacity: 1;
+  }
+}
+
+/* Toast variants */
+.toast-success {
+  border-color: #10b981;
+  background-color: rgba(16, 185, 129, 0.1);
+}
+
+.toast-success::before {
+  content: '✓';
+  color: #10b981;
+  font-weight: bold;
+  flex-shrink: 0;
+}
+
+.toast-error {
+  border-color: #ef4444;
+  background-color: rgba(239, 68, 68, 0.1);
+}
+
+.toast-error::before {
+  content: '✕';
+  color: #ef4444;
+  font-weight: bold;
+  flex-shrink: 0;
+}
+
+.toast-info {
+  border-color: #3b82f6;
+  background-color: rgba(59, 130, 246, 0.1);
+}
+
+.toast-info::before {
+  content: 'ℹ';
+  color: #3b82f6;
+  flex-shrink: 0;
+}
+
+.toast-warning {
+  border-color: #f59e0b;
+  background-color: rgba(245, 158, 11, 0.1);
+}
+
+.toast-warning::before {
+  content: '⚠';
+  color: #f59e0b;
+  flex-shrink: 0;
+}
+
+.toast-close {
+  @apply ml-auto flex-shrink-0 w-6 h-6 flex items-center justify-center cursor-pointer hover:bg-surface rounded transition-colors;
+}
+
+/* Tooltip */
+.tooltip {
+  @apply relative inline-block;
+}
+
+.tooltip-text {
+  @apply absolute bg-bg2 text-text-dim text-xs rounded px-2 py-1 whitespace-nowrap pointer-events-none opacity-0 invisible transition-all duration-200;
+  z-index: 50;
+  bottom: 125%;
+  left: 50%;
+  transform: translateX(-50%);
+}
+
+.tooltip:hover .tooltip-text {
+  @apply opacity-100 visible;
+}
+
+.tooltip-text::after {
+  content: '';
+  @apply absolute w-2 h-2 bg-bg2;
+  bottom: -4px;
+  left: 50%;
+  transform: translateX(-50%) rotate(45deg);
+}
+
+/* Tooltip directions */
+.tooltip-bottom .tooltip-text {
+  @apply top-[125%] bottom-auto;
+}
+
+.tooltip-bottom .tooltip-text::after {
+  @apply top-[-4px] bottom-auto;
+  transform: translateX(-50%) rotate(225deg);
+}
+
+.tooltip-left .tooltip-text {
+  @apply left-auto right-[125%];
+  transform: none;
+}
+
+.tooltip-left .tooltip-text::after {
+  @apply left-auto right-[-4px];
+  transform: rotate(135deg);
+}
+
+.tooltip-right .tooltip-text {
+  @apply left-[125%];
+  transform: none;
+}
+
+.tooltip-right .tooltip-text::after {
+  @apply left-[-4px] right-auto;
+  transform: rotate(315deg);
+}
index eead573a1f9a5240ae28a7c67661e153c0711be6..cf5343a66cf4eb3a5872b615a84b3dbdb2391168 100644 (file)
@@ -2423,6 +2423,790 @@ article.border.border-border\/30.rounded-lg.overflow-hidden.group.bg-bg {
   box-shadow: 0 0 20px var(--accent-glow);
 }
 
+/* ============================================
+   FORM COMPONENTS (Week 4)
+   ============================================ */
+
+/* Form input base styles */
+
+.form-input,
+.form-textarea,
+.form-select {
+  width: 100%;
+  border-radius: 0.25rem;
+  border-width: 1px;
+  border-color: var(--border);
+  background-color: var(--bg2);
+  padding-left: 1rem;
+  padding-right: 1rem;
+  padding-top: 0.5rem;
+  padding-bottom: 0.5rem;
+  font-family: IBM Plex Sans, sans-serif;
+  color: var(--text);
+  transition-property: all;
+  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+  transition-duration: 200ms;
+}
+
+article.form-input.border-border\/30.rounded-lg.overflow-hidden.group.bg-bg,article
+.form-textarea.border-border\/30.rounded-lg.overflow-hidden.group.bg-bg,article
+.form-select.border-border\/30.rounded-lg.overflow-hidden.group.bg-bg {
+  border-color: var(--border);
+  box-shadow: 0 0 20px var(--accent-glow);
+}
+
+/* Input placeholder styling */
+
+.form-input::-moz-placeholder, .form-textarea::-moz-placeholder {
+  color: var(--text-dim);
+  opacity: 0.7;
+}
+
+.form-input::placeholder,
+.form-textarea::placeholder {
+  color: var(--text-dim);
+  opacity: 0.7;
+}
+
+/* Input focus state */
+
+.form-input:focus,
+.form-textarea:focus,
+.form-select:focus {
+  outline: 2px solid transparent;
+  outline-offset: 2px;
+  --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
+  --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
+  box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
+  --tw-ring-color: var(--accent);
+  --tw-ring-offset-width: 2px;
+  ring-offset-color: var(--bg);
+  border-color: var(--accent);
+}
+
+/* Input invalid/error state */
+
+.form-input:invalid,
+.form-textarea:invalid,
+.form-select:invalid,
+.form-input.error,
+.form-textarea.error,
+.form-select.error {
+  --tw-border-opacity: 1;
+  border-color: rgb(239 68 68 / var(--tw-border-opacity, 1));
+  --tw-ring-opacity: 1;
+  --tw-ring-color: rgb(239 68 68 / var(--tw-ring-opacity, 1));
+}
+
+.form-input:invalid:focus,
+.form-textarea:invalid:focus,
+.form-select:invalid:focus {
+  --tw-border-opacity: 1;
+  border-color: rgb(239 68 68 / var(--tw-border-opacity, 1));
+  --tw-ring-opacity: 1;
+  --tw-ring-color: rgb(239 68 68 / var(--tw-ring-opacity, 1));
+  ring-offset-color: var(--bg);
+}
+
+/* Input disabled state */
+
+.form-input:disabled,
+.form-textarea:disabled,
+.form-select:disabled {
+  cursor: not-allowed;
+  background-color: var(--muted);
+  opacity: 0.5;
+}
+
+/* Textarea specific */
+
+.form-textarea {
+  min-height: 8rem;
+  resize: vertical;
+}
+
+.form-textarea.auto-expand {
+  resize: none;
+  overflow-y: hidden;
+}
+
+/* Select dropdown */
+
+.form-select {
+  cursor: pointer;
+  -webkit-appearance: none;
+     -moz-appearance: none;
+          appearance: none;
+  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23a855f7' d='M6 9L1 4h10z'/%3E%3C/svg%3E");
+  background-repeat: no-repeat;
+  background-position: right 0.75rem center;
+  padding-right: 2.5rem;
+}
+
+html.theme-light .form-select {
+  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%239333ea' d='M6 9L1 4h10z'/%3E%3C/svg%3E");
+}
+
+/* Checkbox and radio button base */
+
+.form-checkbox,
+.form-radio {
+  height: 1.25rem;
+  width: 1.25rem;
+  cursor: pointer;
+  accent-color: var(--accent);
+  transition-property: all;
+  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+  transition-duration: 200ms;
+  -webkit-appearance: none;
+     -moz-appearance: none;
+          appearance: none;
+  border: 2px solid var(--border);
+  border-radius: 0.375rem;
+  flex-shrink: 0;
+}
+
+.form-radio {
+  border-radius: 50%;
+}
+
+/* Checkbox/radio focus state */
+
+.form-checkbox:focus-visible,
+.form-radio:focus-visible {
+  outline: 2px solid transparent;
+  outline-offset: 2px;
+  --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
+  --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
+  box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
+  --tw-ring-color: var(--accent);
+  --tw-ring-offset-width: 2px;
+  ring-offset-color: var(--bg);
+}
+
+/* Checkbox/radio checked state */
+
+.form-checkbox:checked,
+.form-radio:checked {
+  background-color: var(--accent);
+  border-color: var(--accent);
+  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3E%3C/svg%3E");
+  background-repeat: no-repeat;
+  background-position: center;
+  background-size: 100%;
+}
+
+.form-radio:checked {
+  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='8' cy='8' r='3.5'/%3E%3C/svg%3E");
+}
+
+/* Checkbox/radio disabled state */
+
+.form-checkbox:disabled,
+.form-radio:disabled {
+  cursor: not-allowed;
+  opacity: 0.5;
+  border-color: var(--muted);
+}
+
+/* Form group layout */
+
+.form-group > :not([hidden]) ~ :not([hidden]) {
+  --tw-space-y-reverse: 0;
+  margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse)));
+  margin-bottom: calc(0.5rem * var(--tw-space-y-reverse));
+}
+
+.form-group label {
+  display: block;
+  font-size: 0.875rem;
+  line-height: 1.25rem;
+  font-weight: 600;
+  color: var(--text);
+}
+
+.form-group.required label::after {
+  content: ' *';
+  color: #ef4444;
+}
+
+.form-group-input {
+  position: relative;
+}
+
+.form-group-help {
+  font-size: 0.75rem;
+  line-height: 1rem;
+  color: var(--text-dim);
+}
+
+.form-group.error .form-group-help {
+  --tw-text-opacity: 1;
+  color: rgb(239 68 68 / var(--tw-text-opacity, 1));
+}
+
+.form-error {
+  margin-top: 0.25rem;
+  font-size: 0.875rem;
+  line-height: 1.25rem;
+  --tw-text-opacity: 1;
+  color: rgb(239 68 68 / var(--tw-text-opacity, 1));
+}
+
+/* Form layout utilities */
+
+.form-row {
+  display: flex;
+  flex-direction: column;
+  gap: 1rem;
+}
+
+@media (min-width: 768px) {
+  .form-row {
+    flex-direction: row;
+  }
+}
+
+.form-row > .form-group {
+  flex: 1 1 0%;
+}
+
+.form-inline {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-end;
+  gap: 1rem;
+}
+
+@media (min-width: 640px) {
+  .form-inline {
+    flex-direction: row;
+  }
+}
+
+.form-inline .form-group {
+  flex: 1 1 0%;
+}
+
+.form-inline .btn {
+  height: 2.5rem;
+}
+
+/* Character count indicator */
+
+.form-char-count {
+  text-align: right;
+  font-size: 0.75rem;
+  line-height: 1rem;
+  color: var(--text-dim);
+}
+
+.form-char-count.warning {
+  --tw-text-opacity: 1;
+  color: rgb(245 158 11 / var(--tw-text-opacity, 1));
+}
+
+.form-char-count.error {
+  --tw-text-opacity: 1;
+  color: rgb(239 68 68 / var(--tw-text-opacity, 1));
+}
+
+/* ============================================
+   MODAL COMPONENTS (Week 4)
+   ============================================ */
+
+/* Modal backdrop */
+
+.modal-backdrop {
+  visibility: hidden;
+  position: fixed;
+  inset: 0px;
+  z-index: 40;
+  background-color: rgb(0 0 0 / 0.5);
+  opacity: 0;
+  transition-property: all;
+  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+  transition-duration: 300ms;
+  -webkit-backdrop-filter: blur(2px);
+          backdrop-filter: blur(2px);
+}
+
+.modal-backdrop.active {
+  visibility: visible;
+  opacity: 1;
+}
+
+/* Modal container */
+
+.modal {
+  visibility: hidden;
+  position: fixed;
+  inset: 0px;
+  z-index: 50;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 1rem;
+  opacity: 0;
+  transition-property: all;
+  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+  transition-duration: 300ms;
+  pointer-events: none;
+}
+
+.modal.active {
+  visibility: visible;
+  opacity: 1;
+  pointer-events: auto;
+}
+
+.modal.active .modal-backdrop {
+  visibility: visible;
+  opacity: 1;
+  pointer-events: auto;
+}
+
+/* Modal content box */
+
+.modal-content {
+  display: flex;
+  max-height: 90vh;
+  width: 100%;
+  max-width: 32rem;
+  flex-direction: column;
+  border-radius: 0.5rem;
+  border-width: 1px;
+  border-color: var(--border);
+  background-color: var(--bg2);
+  --tw-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
+  --tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);
+  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
+}
+
+article.modal-content.border-border\/30.rounded-lg.overflow-hidden.group.bg-bg {
+  border-color: var(--border);
+  box-shadow: 0 0 20px var(--accent-glow);
+}
+
+article.border.border-border\/30.modal-content.overflow-hidden.group.bg-bg {
+  border-color: var(--border);
+  box-shadow: 0 0 20px var(--accent-glow);
+}
+
+.modal-content {
+  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
+  animation: modalSlideUp 0.3s ease-out;
+}
+
+@keyframes modalSlideUp {
+  from {
+    transform: translateY(20px);
+    opacity: 0;
+  }
+
+  to {
+    transform: translateY(0);
+    opacity: 1;
+  }
+}
+
+/* Modal header */
+
+.modal-header {
+  border-color: var(--border);
+}
+
+.frosted-bar.modal-header {
+  border-color: var(--border);
+}
+
+.modal-header {
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+  gap: 1rem;
+  border-bottom-width: 1px;
+  border-color: var(--border);
+  padding: 1.5rem;
+}
+
+.modal-title {
+  font-size: 1.25rem;
+  line-height: 1.75rem;
+  font-weight: 700;
+  color: var(--text);
+}
+
+.modal-close {
+  display: flex;
+  height: 2rem;
+  width: 2rem;
+  cursor: pointer;
+  align-items: center;
+  justify-content: center;
+  border-radius: 0.25rem;
+  transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
+  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+  transition-duration: 150ms;
+}
+
+.modal-close:hover {
+  background-color: var(--surface);
+}
+
+.modal-close::before,
+.modal-close::after {
+  content: '';
+  position: absolute;
+  height: 0.125rem;
+  width: 1.25rem;
+  background-color: var(--text-dim);
+}
+
+.modal-close::before {
+  transform: rotate(45deg);
+}
+
+.modal-close::after {
+  transform: rotate(-45deg);
+}
+
+.modal-close:hover::before,
+.modal-close:hover::after {
+  background-color: var(--accent);
+}
+
+/* Modal body */
+
+.modal-body {
+  flex: 1 1 0%;
+}
+
+.modal-body > :not([hidden]) ~ :not([hidden]) {
+  --tw-space-y-reverse: 0;
+  margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse)));
+  margin-bottom: calc(1rem * var(--tw-space-y-reverse));
+}
+
+.modal-body {
+  overflow-y: auto;
+  padding: 1.5rem;
+}
+
+/* Modal footer */
+
+.modal-footer {
+  border-color: var(--border);
+}
+
+
+  .frosted-bar.modal-footer {
+  border-color: var(--border);
+}
+
+.modal-footer {
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  gap: 0.75rem;
+  border-top-width: 1px;
+  border-color: var(--border);
+  padding: 1.5rem;
+}
+
+/* Modal sizes */
+
+.modal-content.modal-sm {
+  max-width: 24rem;
+}
+
+.modal-content.modal-md {
+  max-width: 28rem;
+}
+
+.modal-content.modal-lg {
+  max-width: 42rem;
+}
+
+/* Modal variants */
+
+.modal-content.modal-alert {
+  max-width: 24rem;
+}
+
+.modal-content.modal-confirm {
+  max-width: 24rem;
+}
+
+/* Modal button styling */
+
+.modal-footer .btn {
+  min-width: 100px;
+}
+
+/* ============================================
+   INTERACTIVE PATTERNS (Week 4)
+   ============================================ */
+
+/* Loading spinner */
+
+.spinner {
+  display: inline-block;
+  width: 1rem;
+  height: 1rem;
+  border: 2px solid var(--border);
+  border-top-color: var(--accent);
+  border-radius: 50%;
+  animation: spin 0.6s linear infinite;
+}
+
+@keyframes spin {
+  to {
+    transform: rotate(360deg);
+  }
+}
+
+.spinner-sm {
+  width: 0.75rem;
+  height: 0.75rem;
+  border-width: 1.5px;
+}
+
+.spinner-lg {
+  width: 1.5rem;
+  height: 1.5rem;
+  border-width: 3px;
+}
+
+/* Button with spinner */
+
+.btn:disabled .spinner {
+  margin-right: 0.5rem;
+  display: inline-block;
+}
+
+/* Toast notification container */
+
+.toast-container {
+  position: fixed;
+  bottom: 0px;
+  right: 0px;
+  z-index: 50;
+  max-width: 24rem;
+}
+
+.toast-container > :not([hidden]) ~ :not([hidden]) {
+  --tw-space-y-reverse: 0;
+  margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse)));
+  margin-bottom: calc(0.75rem * var(--tw-space-y-reverse));
+}
+
+.toast-container {
+  padding: 1rem;
+}
+
+@media (max-width: 640px) {
+  .toast-container {
+    left: 0px;
+    right: 0px;
+    padding-left: 1rem;
+    padding-right: 1rem;
+  }
+}
+
+/* Toast base */
+
+.toast {
+  display: flex;
+  align-items: flex-start;
+  gap: 0.75rem;
+  border-radius: 0.5rem;
+  border-width: 1px;
+  border-color: var(--border);
+  padding: 1rem;
+  --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
+  --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
+  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
+}
+
+article.border.border-border\/30.toast.overflow-hidden.group.bg-bg {
+  border-color: var(--border);
+  box-shadow: 0 0 20px var(--accent-glow);
+}
+
+article.toast.border-border\/30.rounded-lg.overflow-hidden.group.bg-bg {
+  border-color: var(--border);
+  box-shadow: 0 0 20px var(--accent-glow);
+}
+
+.toast {
+  animation: slideInUp 0.3s ease-out;
+  background-color: var(--bg2);
+  color: var(--text);
+}
+
+@keyframes slideInUp {
+  from {
+    transform: translateY(100%);
+    opacity: 0;
+  }
+
+  to {
+    transform: translateY(0);
+    opacity: 1;
+  }
+}
+
+/* Toast variants */
+
+.toast-success {
+  border-color: #10b981;
+  background-color: rgba(16, 185, 129, 0.1);
+}
+
+.toast-success::before {
+  content: '✓';
+  color: #10b981;
+  font-weight: bold;
+  flex-shrink: 0;
+}
+
+.toast-error {
+  border-color: #ef4444;
+  background-color: rgba(239, 68, 68, 0.1);
+}
+
+.toast-error::before {
+  content: '✕';
+  color: #ef4444;
+  font-weight: bold;
+  flex-shrink: 0;
+}
+
+.toast-info {
+  border-color: #3b82f6;
+  background-color: rgba(59, 130, 246, 0.1);
+}
+
+.toast-info::before {
+  content: 'ℹ';
+  color: #3b82f6;
+  flex-shrink: 0;
+}
+
+.toast-warning {
+  border-color: #f59e0b;
+  background-color: rgba(245, 158, 11, 0.1);
+}
+
+.toast-warning::before {
+  content: '⚠';
+  color: #f59e0b;
+  flex-shrink: 0;
+}
+
+.toast-close {
+  margin-left: auto;
+  display: flex;
+  height: 1.5rem;
+  width: 1.5rem;
+  flex-shrink: 0;
+  cursor: pointer;
+  align-items: center;
+  justify-content: center;
+  border-radius: 0.25rem;
+  transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
+  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+  transition-duration: 150ms;
+}
+
+.toast-close:hover {
+  background-color: var(--surface);
+}
+
+/* Tooltip */
+
+.tooltip {
+  position: relative;
+  display: inline-block;
+}
+
+.tooltip-text {
+  pointer-events: none;
+  visibility: hidden;
+  position: absolute;
+  white-space: nowrap;
+  border-radius: 0.25rem;
+  background-color: var(--bg2);
+  padding-left: 0.5rem;
+  padding-right: 0.5rem;
+  padding-top: 0.25rem;
+  padding-bottom: 0.25rem;
+  font-size: 0.75rem;
+  line-height: 1rem;
+  color: var(--text-dim);
+  opacity: 0;
+  transition-property: all;
+  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+  transition-duration: 200ms;
+  z-index: 50;
+  bottom: 125%;
+  left: 50%;
+  transform: translateX(-50%);
+}
+
+.tooltip:hover .tooltip-text {
+  visibility: visible;
+  opacity: 1;
+}
+
+.tooltip-text::after {
+  content: '';
+  position: absolute;
+  height: 0.5rem;
+  width: 0.5rem;
+  background-color: var(--bg2);
+  bottom: -4px;
+  left: 50%;
+  transform: translateX(-50%) rotate(45deg);
+}
+
+/* Tooltip directions */
+
+.tooltip-bottom .tooltip-text {
+  top: 125%;
+  bottom: auto;
+}
+
+.tooltip-bottom .tooltip-text::after {
+  top: -4px;
+  bottom: auto;
+  transform: translateX(-50%) rotate(225deg);
+}
+
+.tooltip-left .tooltip-text {
+  left: auto;
+  right: 125%;
+  transform: none;
+}
+
+.tooltip-left .tooltip-text::after {
+  left: auto;
+  right: -4px;
+  transform: rotate(135deg);
+}
+
+.tooltip-right .tooltip-text {
+  left: 125%;
+  transform: none;
+}
+
+.tooltip-right .tooltip-text::after {
+  left: -4px;
+  right: auto;
+  transform: rotate(315deg);
+}
+
 .hover\:bg-surface:hover {
   background-color: var(--surface);
 }