<template>
  <div class="app-container seat-config-container">
    <!-- 主界面：坐席配置列表 -->
    <template v-if="!showTicketSystem">
      <el-card shadow="never" class="table-header">
        <div class="flex-between">
          <div class="search-box">
            <el-input
              v-model="queryParams.seatName"
              placeholder="请输入坐席名称"
              style="width: 240px"
              clearable
              @keyup.enter="handleQuery"
            >
              <template #append>
                <el-button :icon="Search" @click="handleQuery" />
              </template>
            </el-input>
          </div>
          <div class="btn-group">
            <el-button type="primary" :icon="Plus" @click="handleAdd">创建坐席</el-button>
            <el-button type="success" :icon="Tickets" @click="openTicketSystem">客服工单</el-button>
          </div>
        </div>
      </el-card>

      <el-card shadow="never" class="mt-20">
        <el-table :data="seatList" v-loading="loading" style="width: 100%">
          <el-table-column type="selection" width="55" align="center" />
          <el-table-column label="坐席名称" align="center" prop="seatName" />
          <el-table-column label="坐席头像" align="center" width="100">
            <template #default="scope">
              <el-avatar :size="40" :src="scope.row.avatar" />
            </template>
          </el-table-column>
          <el-table-column label="负责模块" align="center" prop="module">
            <template #default="scope">
              <dict-tag :options="main_agent_module" :value="scope.row.module" />
            </template>
          </el-table-column>
          <el-table-column label="客服姓名" align="left" min-width="200">
            <template #default="scope">
              <div class="waiter-tag-wrap">
                <span v-for="(waiter, index) in scope.row.waiters" :key="index" class="waiter-name">
                  {{ waiter.nickName || waiter.userName }} ({{ waiter.userId }}){{ index < scope.row.waiters.length - 1 ? '、' : '' }}
                </span>
              </div>
            </template>
          </el-table-column>
          <el-table-column label="状态" align="center" prop="status">
            <template #default="scope">
              <el-switch
                v-model="scope.row.status"
                :active-value="1"
                :inactive-value="0"
                @change="handleStatusChange(scope.row)"
              />
            </template>
          </el-table-column>
          <el-table-column label="操作" align="center" width="150">
            <template #default="scope">
              <el-button link type="primary" @click="handleUpdate(scope.row)">编辑</el-button>
              <el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
            </template>
          </el-table-column>
        </el-table>
      </el-card>
    </template>

    <!-- 子界面：客服工单管理系统 (根据图2重构) -->
    <div v-else class="ticket-system-container animate__animated animate__fadeIn">
      <div class="ticket-header">
        <div class="title-row">
          <div class="back-btn" @click="showTicketSystem = false">
            <el-icon><ArrowLeft /></el-icon> 返回坐席配置
          </div>
          <h2 class="sys-title">客服工单中心</h2>
        </div>
        <el-tabs v-model="activeTicketTab" class="ticket-tabs">
          <el-tab-pane label="全部状态" name="all" />
          <el-tab-pane label="待处理" name="pending" />
          <el-tab-pane label="处理中" name="processing" />
          <el-tab-pane label="已完成" name="completed" />
          <el-tab-pane label="已废弃" name="abandoned" />
        </el-tabs>
      </div>

      <el-card shadow="never" class="filter-card mt-20">
        <el-form :inline="true" :model="ticketQuery" class="demo-form-inline">
          <el-form-item label="反馈ID">
            <el-input v-model="ticketQuery.id" placeholder="如：20991216..." clearable />
          </el-form-item>
          <el-form-item label="用户ID">
            <el-input v-model="ticketQuery.userId" placeholder="搜索用户ID" clearable />
          </el-form-item>
          <el-form-item label="反馈渠道">
            <el-select v-model="ticketQuery.source" placeholder="全部渠道" style="width: 140px" clearable>
              <el-option label="小程序" value="小程序" />
              <el-option label="商家" value="商家" />
            </el-select>
          </el-form-item>
          <el-form-item label="问题分类">
            <el-select v-model="ticketQuery.category" placeholder="全部分类" style="width: 140px" clearable>
              <el-option label="这是问题分类1" value="这是问题分类1" />
              <el-option label="测评异常" value="测评异常" />
              <el-option label="功能咨询" value="功能咨询" />
              <el-option label="支付问题" value="支付问题" />
            </el-select>
          </el-form-item>
          <el-form-item>
            <el-button type="primary" :icon="Search" @click="handleTicketSearch">查询</el-button>
            <el-button @click="resetTicketQuery">重置</el-button>
          </el-form-item>
        </el-form>
      </el-card>

      <el-card shadow="never" class="mt-20">
        <el-table :data="ticketList" style="width: 100%" v-loading="ticketLoading">
          <el-table-column prop="id" label="反馈ID" width="140" />
          <el-table-column label="用户信息" width="160">
            <template #default="scope">
              <div class="u-info">
                <span class="u-name">{{ scope.row.userName }}</span>
                <span class="u-id">ID:{{ scope.row.userId }}</span>
              </div>
            </template>
          </el-table-column>
          <el-table-column prop="content" label="反馈内容" min-width="250" show-overflow-tooltip />
          <el-table-column label="状态" width="100" align="center">
            <template #default="scope">
              <el-tag :type="getStatusType(scope.row.status)" effect="dark" size="small">
                {{ getStatusLabel(scope.row.status) }}
              </el-tag>
            </template>
          </el-table-column>
          <el-table-column prop="source" label="反馈渠道" width="120" align="center" />
          <el-table-column prop="category" label="问题分类" width="140" align="center" />
          <el-table-column prop="createTime" label="创建时间" width="180" align="center" />
          <el-table-column prop="finishTime" label="完结时间" width="180" align="center">
            <template #default="scope">{{ scope.row.finishTime || '-' }}</template>
          </el-table-column>
          <el-table-column label="操作" width="120" fixed="right" align="center">
            <template #default="scope">
              <el-button 
                v-if="scope.row.status === 'pending' || scope.row.status === 'processing'"
                link 
                type="primary" 
                @click="handleTicketDetail(scope.row)"
              >处理</el-button>
              <el-button 
                link 
                type="info" 
                @click="handleTicketDetail(scope.row)"
              >详情</el-button>
            </template>
          </el-table-column>
        </el-table>
        <div class="pagination-wrap">
          <el-pagination 
            background 
            layout="prev, pager, next, total" 
            :total="ticketTotal" 
            v-model:current-page="ticketQuery.pageNum"
            v-model:page-size="ticketQuery.pageSize"
            @current-change="getTicketList"
          />
        </div>
      </el-card>
    </div>

    <!-- 工单详情与处理对话框 -->
    <el-dialog
      v-model="ticketDetailOpen"
      title="工单业务处理"
      width="1100px"
      custom-class="ticket-detail-dialog"
      append-to-body
    >
      <div class="ticket-detail-wrapper">
        <!-- 左侧工单快照 -->
        <div class="ticket-snapshot">
          <div class="snap-title">工单基础信息</div>
          <el-descriptions :column="1" border>
            <el-descriptions-item label="反馈ID">{{ currentTicket?.id }}</el-descriptions-item>
            <el-descriptions-item label="反馈状态">
              <el-tag size="small">{{ getStatusLabel(currentTicket?.status) }}</el-tag>
            </el-descriptions-item>
            <el-descriptions-item label="反馈人">{{ currentTicket?.userName }} ({{ currentTicket?.userId }})</el-descriptions-item>
            <el-descriptions-item label="渠道">{{ currentTicket?.source }}</el-descriptions-item>
            <el-descriptions-item label="原始反馈内容">
              <div class="raw-content">{{ currentTicket?.content }}</div>
            </el-descriptions-item>
          </el-descriptions>
          <div class="snap-title mt-20">处理操作</div>
          <el-form label-position="top">
            <el-form-item label="流转状态">
              <el-radio-group v-model="ticketHandleForm.status" size="small">
                <el-radio-button label="processing">处理中</el-radio-button>
                <el-radio-button label="completed">已完成</el-radio-button>
                <el-radio-button label="abandoned">废弃</el-radio-button>
              </el-radio-group>
            </el-form-item>
            <el-form-item label="快捷回复">
              <el-input 
                type="textarea" 
                :rows="4" 
                v-model="ticketHandleForm.reply" 
                placeholder="请输入处理意见或回复用户的内容..."
              />
            </el-form-item>
            <el-button type="primary" class="full-width" @click="submitTicketHandle">提交处理结果</el-button>
          </el-form>
        </div>

        <!-- 右侧对话追踪流 (模仿图2下方效果) -->
        <div class="ticket-chat-tracker">
          <div class="tracker-header">沟通过程溯源</div>
          <div class="chat-flow-container">
            <div 
              v-for="(chat, index) in ticketChats" 
              :key="index" 
              :class="['chat-bubble-row', chat.sender === 'user' ? 'user' : 'waiter']"
            >
              <el-avatar :size="40" :src="chat.avatar" />
              <div class="chat-content-box">
                <div class="chat-meta">{{ chat.name }} · {{ chat.time }}</div>
                <div class="chat-bubble">{{ chat.content }}</div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </el-dialog>

    <!-- 添加或修改坐席对话框 (原有逻辑) -->
    <el-dialog :title="title" v-model="open" width="500px" append-to-body>
      <el-form ref="seatRef" :model="form" :rules="rules" label-width="100px">
        <el-form-item label="坐席名称" prop="seatName">
          <el-input v-model="form.seatName" placeholder="请输入坐席名称" />
        </el-form-item>
        <el-form-item label="坐席头像" prop="avatar">
          <div class="avatar-uploader-box" @click="triggerAvatarUpload" :class="{'has-image': !!avatarPreviewUrl}">
            <template v-if="avatarPreviewUrl">
              <el-image :src="avatarPreviewUrl" class="avatar-preview" fit="cover" />
              <div class="avatar-hover-mask">
                <el-icon><Picture /></el-icon>
                <span>点击更换头像</span>
              </div>
            </template>
            <div v-else class="avatar-placeholder">
              <el-icon><Plus /></el-icon>
              <div class="placeholder-text">添加头像</div>
            </div>
            <input type="file" ref="avatarInput" style="display: none" accept="image/*" @change="handleAvatarUpload" />
          </div>
        </el-form-item>
        <el-form-item label="启用状态" prop="status">
          <el-switch v-model="form.status" :active-value="1" :inactive-value="0" />
        </el-form-item>
        <el-form-item label="选择客服" prop="waiterIds">
          <el-select v-model="form.waiterIds" multiple placeholder="请选择客服" style="width: 100%">
            <el-option v-for="item in waiterOptions" :key="item.userId" :label="item.nickName" :value="item.userId" />
          </el-select>
        </el-form-item>
        <el-form-item label="负责模块" prop="module">
          <el-select v-model="form.module" placeholder="请选择负责模块" style="width: 100%">
            <el-option
              v-for="dict in main_agent_module"
              :key="dict.value"
              :label="dict.label"
              :value="dict.value"
            />
          </el-select>
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="open = false">取 消</el-button>
          <el-button type="primary" @click="submitForm">确 定</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>

<script setup name="CustConfig">
import { ref, reactive, computed, onMounted, getCurrentInstance } from 'vue';
import { Search, Plus, Tickets, ArrowLeft, UserFilled, Picture } from '@element-plus/icons-vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { getSeatList, getSeatDetail, addSeat, updateSeat, deleteSeat, updateSeatStatus } from '@/api/chat/seat';
import { uploadOss } from '@/api/system/oss';
import { listUser } from '@/api/system/user';
import { listTicket, getTicket, processTicket } from '@/api/chat/ticket';

const { proxy } = getCurrentInstance();
const dictData = proxy.useDict("main_agent_module");
const main_agent_module = computed(() => dictData.main_agent_module);

// --- 坐席配置逻辑 ---
const loading = ref(false);
const total = ref(0);
const seatList = ref([]);
const title = ref("");
const open = ref(false);
const waiterOptions = ref([]); // 这里存放所有可选的系统用户
const queryParams = reactive({
  pageNum: 1,
  pageSize: 10,
  seatName: undefined,
  module: undefined
});
const form = ref({});
const seatRef = ref(null);
const avatarPreviewUrl = ref("");

const rules = {
  seatName: [{ required: true, message: "坐席名称不能为空", trigger: "blur" }],
  module: [{ required: true, message: "负责模块不能为空", trigger: "change" }]
};

const avatarInput = ref(null);
function triggerAvatarUpload() { avatarInput.value.click(); }
async function handleAvatarUpload(e) {
  const file = e.target.files[0];
  if (!file) return;
  try {
    const res = await uploadOss(file);
    if (res.code === 200) {
      // 成功获得系统的 ossId 和 url
      form.value.avatar = res.data.url; 
      avatarPreviewUrl.value = res.data.url;
      ElMessage.success('头像上传成功');
    }
  } catch (error) {
    ElMessage.error('头像上传失败');
  }
}

async function getList() {
  loading.value = true;
  try {
    const res = await getSeatList(queryParams);
    if (res.code === 200 || res.code === 0) {
      // 确保后端返回的是 TableDataInfo 格式
      seatList.value = res.rows || [];
      total.value = res.total || 0;
    }
  } catch (error) {
    console.error('获取坐席列表失败:', error);
  } finally {
    loading.value = false;
  }
}

/** 查询客服用户列表 */
async function getWaiterOptions() {
  try {
    // 假设客服用户在某个特定的部门或角色下，这里简单获取所有活动用户
    const res = await listUser({ status: '0', pageNum: 1, pageSize: 1000 });
    if (res.code === 200) {
      waiterOptions.value = res.rows;
    }
  } catch (error) {
    console.error('获取客服列表失败:', error);
  }
}

function handleAdd() { 
  reset(); 
  title.value = "新增坐席"; 
  open.value = true; 
}

async function handleUpdate(row) { 
  reset(); 
  try {
    const res = await getSeatDetail(row.id);
    if (res.code === 200 || res.code === 0) {
      form.value = res.data;
      // 设置头像预览（如果后端返回的是URL）
      avatarPreviewUrl.value = res.data.avatar;
      // 转换关联的客服ID列表供下拉框显示
      if (res.data.waiters) {
        form.value.waiterIds = res.data.waiters.map(w => w.userId);
      }
      title.value = "编辑坐席"; 
      open.value = true;
    }
  } catch (error) {
    console.error('获取坐席详情失败:', error);
  }
}

function reset() { 
  form.value = { id: undefined, seatName: undefined, avatar: undefined, status: 1, waiterIds: [], module: undefined }; 
  avatarPreviewUrl.value = "";
}

async function submitForm() { 
  seatRef.value.validate(async valid => {
    if (valid) {
      try {
        const data = { ...form.value };
        if (data.id) {
          await updateSeat(data.id, data);
          ElMessage.success('修改成功');
        } else {
          await addSeat(data);
          ElMessage.success('新增成功');
        }
        open.value = false;
        getList();
      } catch (error) {
        console.error('保存坐席失败:', error);
      }
    }
  });
}

function handleDelete(row) { 
  ElMessageBox.confirm('确认要删除该坐席吗？', '警告', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(async () => {
    try {
      await deleteSeat(row.id);
      ElMessage.success('删除成功');
      getList();
    } catch (error) {
      console.error('删除坐席失败:', error);
    }
  }).catch(() => {});
}

async function handleStatusChange(row) { 
  try {
    await updateSeatStatus(row.id, row.status);
    ElMessage.success(row.status === 1 ? '已启用' : '已停用');
  } catch (error) {
    // 恢复状态
    row.status = row.status === 1 ? 0 : 1;
    console.error('修改状态失败:', error);
  }
}

function handleQuery() { 
  queryParams.pageNum = 1;
  getList();
}

onMounted(() => {
  getList();
  getWaiterOptions();
});

// --- 新增：客服工单子系统逻辑 ---
const showTicketSystem = ref(false);
const activeTicketTab = ref('all');
const ticketTotal = ref(0);
const ticketQuery = reactive({ pageNum: 1, pageSize: 10, id: '', userId: '', source: '', category: '' });
const ticketDetailOpen = ref(false);
const currentTicket = ref(null);
const ticketHandleForm = reactive({ status: 'processing', reply: '' });
const ticketLoading = ref(false);
const ticketList = ref([]);
const ticketChats = ref([]);

/** 获取工单列表 */
async function getTicketList() {
  ticketLoading.value = true;
  try {
    const params = { ...ticketQuery };
    if (activeTicketTab.value !== 'all') {
      params.status = activeTicketTab.value;
    }
    const res = await listTicket(params);
    ticketList.value = res.rows;
    ticketTotal.value = res.total;
  } catch (err) {
    console.error('获取工单列表失败:', err);
  } finally {
    ticketLoading.value = false;
  }
}

/** 开启工单系统视图 */
function openTicketSystem() {
  showTicketSystem.value = true;
  getTicketList();
}

/** 监听 Tab 切换 */
import { watch } from 'vue';
watch(activeTicketTab, () => {
  ticketQuery.pageNum = 1;
  getTicketList();
});

const handleTicketSearch = () => {
  ticketQuery.pageNum = 1;
  getTicketList();
};

function getStatusType(status) {
  const map = { pending: 'danger', processing: 'primary', completed: 'success', abandoned: 'info' };
  return map[status] || 'info';
}
function getStatusLabel(status) {
  const map = { pending: '待处理', processing: '处理中', completed: '已完成', abandoned: '已废弃' };
  return map[status] || '未知';
}

/** 查询详情并转化对话流 */
async function handleTicketDetail(row) {
  try {
    const res = await getTicket(row.id);
    if (res.code === 200) {
      currentTicket.value = res.data;
      ticketDetailOpen.value = true;
      ticketHandleForm.status = res.data.status === 'pending' ? 'processing' : res.data.status;
      ticketHandleForm.reply = res.data.handlerReply || '';
      
      // 转化消息历史
      if (res.data.messages) {
        ticketChats.value = res.data.messages.map(m => ({
          sender: m.senderRole === 'user' ? 'user' : 'waiter',
          name: m.senderName,
          avatar: m.senderAvatar,
          content: m.content,
          time: m.sendTime
        }));
      } else {
        ticketChats.value = [];
      }
    }
  } catch (err) {
    console.error('获取工单详情失败:', err);
  }
}

function resetTicketQuery() {
  Object.keys(ticketQuery).forEach(key => {
    if (key === 'pageNum') ticketQuery[key] = 1;
    else if (key === 'pageSize') ticketQuery[key] = 10;
    else ticketQuery[key] = '';
  });
  handleTicketSearch();
}

/** 提交处理回复 */
async function submitTicketHandle() {
  if (!ticketHandleForm.reply) {
    ElMessage.warning('请输入处理意见或回复内容');
    return;
  }
  try {
    const data = {
      id: currentTicket.value.id,
      status: ticketHandleForm.status,
      handlerReply: ticketHandleForm.reply
    };
    const res = await processTicket(data);
    if (res.code === 200) {
      ElMessage.success('工单处理已提交');
      ticketDetailOpen.value = false;
      getTicketList();
    }
  } catch (err) {
    console.error('提交处理失败:', err);
  }
}
</script>

<style scoped lang="scss">
.seat-config-container {
  background-color: #f8fafc;
  min-height: calc(100vh - 40px);
  padding: 24px;
  box-sizing: border-box;

  .flex-between { display: flex; justify-content: space-between; align-items: center; }
  .mt-10 { margin-top: 10px; }
  .mt-20 { margin-top: 20px; }

  .table-header { border: none; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.04); }

  /* 坐席列表特有 */
  .waiter-tag-wrap { color: #64748b; font-size: 13px; .waiter-name { display: inline-block; margin-bottom: 2px; } }
  
  .mb-10 { margin-bottom: 10px; }

  /* 工单系统样式 (现代化重构) */
  .ticket-system-container {
    .ticket-header {
      background: #fff; padding: 24px 24px 0; border-radius: 12px 12px 0 0; box-shadow: 0 2px 10px rgba(0,0,0,0.02);
      .title-row {
        display: flex; align-items: center; gap: 20px; margin-bottom: 20px;
        .back-btn { display: flex; align-items: center; gap: 6px; font-size: 14px; color: #64748b; cursor: pointer; &:hover { color: #2563eb; } }
        .sys-title { margin: 0; font-size: 20px; font-weight: 800; color: #1e293b; }
      }
      .ticket-tabs { :deep(.el-tabs__item) { font-size: 15px; font-weight: 600; padding: 0 25px; } }
    }
    .filter-card { border: none; border-radius: 0 0 12px 12px; }
    .u-info { display: flex; flex-direction: column; .u-name { font-weight: 700; color: #1e293b; } .u-id { font-size: 11px; color: #94a3b8; margin-top: 2px; } }
    .pagination-wrap { display: flex; justify-content: flex-end; padding: 20px 0 0; }
  }

  /* 通用 */
  :deep(.el-card) { border: none; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.04); }
}

/* 独立样式：兼容 append-to-body 弹窗中的上传组件 */
.avatar-uploader-box {
  width: 120px; height: 120px; border: 2px dashed #b0b8c1; border-radius: 12px;
  background: #fdfdfd; position: relative; cursor: pointer; overflow: hidden;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  transition: all 0.3s ease;
  margin-top: 5px;
  
  &:hover { 
    border-color: #2563eb; 
    background: #eff6ff;
    box-shadow: 0 8px 24px rgba(37, 99, 235, 0.1);
    transform: translateY(-2px);
    .avatar-hover-mask { opacity: 1; }
  }
  
  &.has-image { border-style: solid; border-color: #f1f5f9; }

  .avatar-preview { width: 100%; height: 100%; object-fit: cover;}
  
  .avatar-placeholder {
    display: flex; flex-direction: column; align-items: center; gap: 8px;
    color: #64748b;
    .el-icon { font-size: 32px; font-weight: 300; }
    .placeholder-text { font-size: 13px; font-weight: 600; }
  }

  .avatar-hover-mask {
    position: absolute; inset: 0; background: rgba(15, 23, 42, 0.6); color: #fff;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 6px; font-size: 11px; opacity: 0; transition: 0.2s;
    backdrop-filter: blur(2px);
    .el-icon { font-size: 22px; }
  }
}

/* 工单详情对话框样式 */
.ticket-detail-wrapper {
  display: flex; gap: 24px; height: 600px;
  
  .ticket-snapshot {
    flex: 1; border-right: 1px solid #f1f5f9; padding-right: 24px; overflow-y: auto;
    .snap-title { font-size: 16px; font-weight: 800; color: #1e293b; margin-bottom: 16px; display: flex; align-items: center; gap: 8px; &::before { content: ''; width: 4px; height: 16px; background: #2563eb; border-radius: 2px; margin-right: 10px; } }
    .raw-content { background: #f8fafc; padding: 12px; border-radius: 8px; font-size: 13px; color: #475569; line-height: 1.6; }
    .full-width { width: 100%; margin-top: 20px; font-weight: 700; height: 40px; }
  }

  .ticket-chat-tracker {
    flex: 1.5; display: flex; flex-direction: column;
    .tracker-header { font-size: 16px; font-weight: 800; color: #1e293b; margin-bottom: 20px; }
    .chat-flow-container {
      flex: 1; background: #f9fafb; border-radius: 12px; padding: 24px; overflow-y: auto; display: flex; flex-direction: column; gap: 20px;
      .chat-bubble-row {
        display: flex; gap: 12px;
        &.user { flex-direction: row; .chat-content-box { .chat-bubble { background: #fff; border: 1px solid #eef2f6; border-radius: 2px 16px 16px 16px; } } }
        &.waiter { flex-direction: row-reverse; .chat-content-box { align-items: flex-end; .chat-meta { text-align: right; } .chat-bubble { background: #ecfdf5; border: 1px solid #d1fae5; color: #065f46; border-radius: 16px 2px 16px 16px; } } }
        .chat-content-box {
          display: flex; flex-direction: column; max-width: 80%;
          .chat-meta { font-size: 12px; color: #94a3b8; margin-bottom: 6px; }
          .chat-bubble { padding: 12px 16px; font-size: 14px; line-height: 1.6; box-shadow: 0 4px 6px rgba(0,0,0,0.02); }
        }
      }
    }
  }
}

.animate__fadeIn { animation: fadeIn 0.4s; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
</style>
