templates/application/whileresume/website/page_article.html.twig line 1

Open in your IDE?
  1. {% extends 'application/whileresume/website/layout-social.html.twig' %}
  2. {% set paramArticle = getCoreToolsList("article") %}
  3. {% block title %}{{ article.title }}{% endblock title %}
  4. {% block description %}{{ article.shortDescription }}{% endblock description %}
  5. {% block robots %}index,follow{% endblock robots %}
  6. {% block meta_social %}{{ parent() }}{% include "/vitrine/components/socialmedia_articles.html.twig" with { 'social_type':'article','article':article } %}{% endblock meta_social %}
  7. {% block meta %}{{ parent() }}{% if article.author is not null and article.author is not empty %}<meta name="author" content="{{ article.author }}" />{% endif %}{% endblock meta %}
  8. {% block canonical %}{% include "/vitrine/lexend/articles/components/canonical.html.twig" with {'article':article} %}{% endblock canonical %}
  9. {% block css %}
  10.     {{ parent() }}
  11.     <style>
  12.         /* ═══════════════════════════════════════════════════════════════
  13.            PAGE ARTICLE — pattern jobs/show (1 colonne max-width 880px)
  14.         ═══════════════════════════════════════════════════════════════ */
  15.         .article-layout{display:block;max-width:880px;margin:0 auto}
  16.         /* ─── Header card (titre + meta) ─── */
  17.         .article-card-main{position:relative;background:#fff;border-radius:16px;box-shadow:0 4px 20px 0 rgba(0,0,0,0.06);overflow:hidden;margin-bottom:14px}
  18.         .article-card-cover{position:relative;width:100%;height:180px;background:linear-gradient(135deg,#6C3AED 0%,#8B5CF6 50%,#A78BFA 100%);overflow:hidden}
  19.         .article-card-cover img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
  20.         .article-card-cover-overlay{position:absolute;inset:0;background:linear-gradient(to bottom,rgba(0,0,0,.1) 0%,rgba(0,0,0,.55) 100%)}
  21.         @media(max-width:480px){.article-card-cover{height:140px}}
  22.         .article-card-head{padding:20px 24px 18px}
  23.         @media(max-width:480px){.article-card-head{padding:16px 18px 14px}}
  24.         .article-card-title{font-size:24px;font-weight:800;color:#1E1B2E;line-height:1.25;letter-spacing:-0.02em;margin:0 0 10px}
  25.         @media(min-width:768px){.article-card-title{font-size:28px}}
  26.         @media(max-width:480px){.article-card-title{font-size:22px}}
  27.         .article-card-subtitle{font-size:15px;color:#4B5563;line-height:1.5;margin:0 0 14px}
  28.         @media(max-width:480px){.article-card-subtitle{font-size:14px}}
  29.         .article-card-meta{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:0}
  30.         .article-card-meta-item{display:inline-flex;align-items:center;gap:5px;padding:5px 11px;border-radius:100px;background:#F3F4F6;font-size:12px;color:#4B5563;font-weight:500}
  31.         .article-card-meta-item svg{width:12px;height:12px;flex-shrink:0;color:#6B7280}
  32.         .article-card-meta-featured{background:#FEF3C7;color:#92400E;font-weight:600}
  33.         .article-card-meta-featured svg{color:currentColor}
  34.         /* ─── TOC sommaire flottant (bas à gauche, minimaliste) ─── */
  35.         .article-toc-fab{
  36.             position:fixed;
  37.             left:20px;
  38.             bottom:20px;
  39.             z-index:100;
  40.         }
  41.         @media(max-width:991px){
  42.             .article-toc-fab{left:16px;bottom:16px}
  43.         }
  44.         /* Bouton FAB (état fermé) */
  45.         .article-toc-trigger{
  46.             display:inline-flex;align-items:center;gap:8px;
  47.             background:#1E1B2E;
  48.             color:#fff;
  49.             border:none;
  50.             border-radius:100px;
  51.             padding:11px 18px 11px 14px;
  52.             font-size:13px;font-weight:600;
  53.             font-family:inherit;
  54.             cursor:pointer;
  55.             box-shadow:0 8px 24px rgba(30,27,46,.28),0 2px 6px rgba(30,27,46,.18);
  56.             transition:transform .2s ease,box-shadow .2s ease,background .15s ease;
  57.         }
  58.         .article-toc-trigger:hover{
  59.             transform:translateY(-2px);
  60.             box-shadow:0 12px 30px rgba(30,27,46,.35),0 3px 8px rgba(30,27,46,.22);
  61.             background:var(--theme-color,#6C3AED);
  62.         }
  63.         .article-toc-trigger-icon{
  64.             display:inline-flex;align-items:center;justify-content:center;
  65.             width:24px;height:24px;border-radius:50%;
  66.             background:rgba(255,255,255,.15);
  67.             flex-shrink:0;
  68.         }
  69.         .article-toc-trigger-icon svg{width:12px;height:12px}
  70.         .article-toc-trigger-count{
  71.             display:inline-flex;align-items:center;justify-content:center;
  72.             min-width:20px;height:20px;padding:0 6px;border-radius:100px;
  73.             background:var(--theme-color,#6C3AED);
  74.             color:#fff;
  75.             font-size:10.5px;font-weight:700;
  76.             line-height:1;
  77.             margin-left:2px;
  78.         }
  79.         .article-toc-trigger.has-active .article-toc-trigger-count{
  80.             background:#fff;
  81.             color:var(--theme-color,#6C3AED);
  82.         }
  83.         @media(max-width:540px){
  84.             .article-toc-trigger-label{display:none}
  85.             .article-toc-trigger{padding:11px 12px}
  86.         }
  87.         /* Panneau (état ouvert) */
  88.         .article-toc-panel{
  89.             position:absolute;
  90.             left:0;
  91.             bottom:calc(100% + 10px);
  92.             width:320px;
  93.             max-width:calc(100vw - 40px);
  94.             background:#fff;
  95.             border-radius:16px;
  96.             box-shadow:0 16px 48px rgba(0,0,0,.18),0 4px 12px rgba(0,0,0,.08);
  97.             opacity:0;
  98.             transform:translateY(8px) scale(.96);
  99.             transform-origin:bottom left;
  100.             pointer-events:none;
  101.             transition:opacity .2s ease,transform .2s ease;
  102.             overflow:hidden;
  103.         }
  104.         .article-toc-fab.is-open .article-toc-panel{
  105.             opacity:1;
  106.             transform:translateY(0) scale(1);
  107.             pointer-events:auto;
  108.         }
  109.         @media(max-width:540px){
  110.             .article-toc-panel{width:calc(100vw - 32px);max-width:340px}
  111.         }
  112.         .article-toc-panel-header{
  113.             display:flex;align-items:center;gap:10px;
  114.             padding:14px 16px;
  115.             border-bottom:1px solid #F3F4F6;
  116.         }
  117.         .article-toc-panel-icon{
  118.             display:inline-flex;align-items:center;justify-content:center;
  119.             width:28px;height:28px;border-radius:8px;
  120.             background:#F5F3FF;
  121.             color:var(--theme-color,#6C3AED);
  122.             flex-shrink:0;
  123.         }
  124.         .article-toc-panel-icon svg{width:13px;height:13px}
  125.         .article-toc-panel-label{
  126.             font-size:11px;font-weight:700;
  127.             color:#1E1B2E;
  128.             text-transform:uppercase;letter-spacing:.08em;
  129.             flex:1;margin:0;
  130.         }
  131.         .article-toc-panel-close{
  132.             background:transparent;border:0;
  133.             color:#9CA3AF;cursor:pointer;
  134.             padding:4px;line-height:1;
  135.             display:inline-flex;
  136.             border-radius:6px;
  137.             transition:background .15s,color .15s;
  138.         }
  139.         .article-toc-panel-close:hover{background:#F3F4F6;color:#1E1B2E}
  140.         .article-toc-panel-close svg{width:14px;height:14px}
  141.         .article-toc-panel-body{
  142.             padding:14px 16px;
  143.             max-height:60vh;
  144.             overflow-y:auto;
  145.         }
  146.         .article-toc-panel-body::-webkit-scrollbar{width:4px}
  147.         .article-toc-panel-body::-webkit-scrollbar-thumb{background:#E5E7EB;border-radius:4px}
  148.         .article-summary ul,.article-summary ol{padding-left:0;margin:0;list-style:none}
  149.         .article-summary li{font-size:13px;line-height:1.5;margin-bottom:6px;font-weight:500;position:relative;padding-left:14px}
  150.         .article-summary li::before{content:"";position:absolute;left:0;top:8px;width:5px;height:5px;border-radius:50%;background:#D1D5DB;transition:background .15s ease,transform .15s ease}
  151.         .article-summary li:hover::before{background:var(--theme-color,#6C3AED);transform:scale(1.4)}
  152.         .article-summary li a{color:#4B5563;text-decoration:none;transition:color .15s ease;display:block}
  153.         .article-summary li a:hover{color:var(--theme-color,#6C3AED)}
  154.         .article-summary li.active>a{color:var(--theme-color,#6C3AED);font-weight:700}
  155.         .article-summary li.active::before{background:var(--theme-color,#6C3AED);transform:scale(1.4)}
  156.         .article-summary ul ul,.article-summary ol ol{margin-top:6px;padding-left:14px}
  157.         /* Backdrop discret (clic en dehors pour fermer) */
  158.         .article-toc-backdrop{
  159.             position:fixed;inset:0;
  160.             background:transparent;
  161.             z-index:99;
  162.             opacity:0;
  163.             pointer-events:none;
  164.             transition:opacity .2s ease;
  165.         }
  166.         .article-toc-backdrop.is-visible{
  167.             opacity:1;
  168.             pointer-events:auto;
  169.         }
  170.         /* ─── Contenu de l'article ─── */
  171.         .article-card-content{background:#fff;border-radius:16px;box-shadow:0 4px 20px 0 rgba(0,0,0,0.06);overflow:hidden;margin-bottom:14px}
  172.         .article-card-section{padding:24px}
  173.         @media(max-width:480px){.article-card-section{padding:18px}}
  174.         .article-content{font-size:15px;line-height:1.75;color:#374151}
  175.         .article-content h2{font-size:22px;font-weight:700;color:#1E1B2E;margin:28px 0 12px;line-height:1.3;letter-spacing:-0.01em;scroll-margin-top:80px}
  176.         .article-content h2:first-child{margin-top:0}
  177.         .article-content h3{font-size:18px;font-weight:700;color:#1E1B2E;margin:22px 0 10px;line-height:1.3;scroll-margin-top:80px}
  178.         .article-content h4{font-size:16px;font-weight:700;color:#1E1B2E;margin:18px 0 8px}
  179.         .article-content p{margin-bottom:14px}
  180.         .article-content a{color:var(--theme-color,#6C3AED);text-decoration:none;font-weight:500}
  181.         .article-content a:hover{text-decoration:underline}
  182.         .article-content ul,.article-content ol{padding-left:24px;margin-bottom:14px}
  183.         .article-content li{margin-bottom:6px}
  184.         .article-content img{max-width:100%;height:auto;border-radius:12px;margin:16px 0}
  185.         .article-content blockquote{border-left:4px solid var(--theme-color,#6C3AED);padding:10px 18px;margin:16px 0;background:#F5F3FF;border-radius:0 8px 8px 0;font-style:italic;color:#4B5563}
  186.         .article-content code{background:#F3F4F6;padding:2px 6px;border-radius:4px;font-size:13px;color:#D6336C;font-family:"SF Mono",Menlo,Monaco,Consolas,monospace}
  187.         .article-content pre{background:#1E1B2E;color:#F3F4F6;padding:18px;border-radius:10px;overflow-x:auto;margin:16px 0;font-size:13px;line-height:1.6}
  188.         .article-content pre code{background:transparent;color:inherit;padding:0}
  189.         .article-content table{width:100%;border-collapse:collapse;margin:16px 0;font-size:14px}
  190.         .article-content th,.article-content td{padding:10px 12px;border:1px solid #E5E7EB;text-align:left}
  191.         .article-content th{background:#F9FAFB;font-weight:700;color:#1E1B2E}
  192.         /* ─── Tags en bas du contenu ─── */
  193.         .article-tags{display:flex;flex-wrap:wrap;gap:6px;margin-top:24px;padding-top:18px;border-top:1px solid #F3F4F6}
  194.         .article-tag{display:inline-flex;align-items:center;padding:5px 11px;border-radius:100px;background:#F5F3FF;color:var(--theme-color,#6C3AED);font-size:12px;font-weight:600;text-decoration:none;transition:background .15s,transform .15s}
  195.         .article-tag:hover{background:#EDE9FE;transform:translateY(-1px);color:var(--theme-color,#6C3AED)}
  196.         .article-tag::before{content:"#";opacity:.6;margin-right:1px}
  197.         /* ─── CTA inline (Recruteur / Candidat) ─── */
  198.         .article-cta{position:relative;border-radius:18px;padding:24px;margin-bottom:14px;background:linear-gradient(135deg,#6C3AED 0%,#8B5CF6 50%,#A78BFA 100%);overflow:hidden;box-shadow:0 10px 30px -8px rgba(108,58,237,.35)}
  199.         .article-cta::before{content:"";position:absolute;top:-40px;right:-40px;width:180px;height:180px;background:radial-gradient(circle,rgba(255,255,255,.15) 0%,transparent 70%);pointer-events:none}
  200.         .article-cta::after{content:"";position:absolute;bottom:-60px;left:-60px;width:200px;height:200px;background:radial-gradient(circle,rgba(255,255,255,.08) 0%,transparent 70%);pointer-events:none}
  201.         .article-cta-eyebrow{display:inline-flex;align-items:center;gap:6px;font-size:11px;font-weight:700;color:#fff;text-transform:uppercase;letter-spacing:.1em;background:rgba(255,255,255,.18);padding:5px 12px;border-radius:100px;margin-bottom:12px;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);position:relative;z-index:2}
  202.         .article-cta-eyebrow svg{width:12px;height:12px}
  203.         .article-cta-title{font-size:20px;font-weight:800;color:#fff;line-height:1.25;letter-spacing:-0.01em;margin:0 0 18px;position:relative;z-index:2}
  204.         @media(min-width:768px){.article-cta-title{font-size:24px}}
  205.         .article-cta-grid{display:grid;grid-template-columns:1fr;gap:12px;position:relative;z-index:2}
  206.         @media(min-width:640px){.article-cta-grid{grid-template-columns:1fr 1fr;gap:14px}}
  207.         .article-cta-side{background:rgba(255,255,255,.97);border-radius:14px;padding:18px;display:flex;flex-direction:column;transition:transform .2s,box-shadow .2s}
  208.         .article-cta-side:hover{transform:translateY(-2px);box-shadow:0 12px 24px -8px rgba(0,0,0,.18)}
  209.         .article-cta-side-head{display:flex;align-items:center;gap:10px;margin-bottom:10px}
  210.         .article-cta-side-icon{width:38px;height:38px;border-radius:10px;display:inline-flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#EDE9FE,#DDD6FE);color:var(--theme-color,#6C3AED);flex-shrink:0}
  211.         .article-cta-side-icon svg{width:18px;height:18px}
  212.         .article-cta-side-label{font-size:11px;font-weight:700;color:var(--theme-color,#6C3AED);text-transform:uppercase;letter-spacing:.08em;margin:0}
  213.         .article-cta-side-heading{font-size:15px;font-weight:700;color:#1E1B2E;line-height:1.3;margin:0 0 8px}
  214.         .article-cta-side-text{font-size:13px;color:#6B7280;line-height:1.5;margin:0 0 14px;flex:1}
  215.         .article-cta-side-btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;background:var(--theme-color,#6C3AED);color:#fff;text-decoration:none;padding:11px 16px;border-radius:10px;font-size:13px;font-weight:700;letter-spacing:.01em;transition:background .15s,transform .15s}
  216.         .article-cta-side-btn:hover{background:#5B21B6;color:#fff;transform:translateX(2px)}
  217.         .article-cta-side-btn svg{width:14px;height:14px;transition:transform .15s}
  218.         .article-cta-side-btn:hover svg{transform:translateX(2px)}
  219.         /* ─── Articles similaires (style identique aux jobs) ─── */
  220.         .similar-section-title{font-size:11px;font-weight:700;color:#9CA3AF;text-transform:uppercase;letter-spacing:.08em;margin:32px 0 12px;padding-left:4px;display:inline-flex;align-items:center;gap:8px}
  221.         .similar-card{background:#fff;border-radius:14px;padding:14px;box-shadow:0 0 16px 0 rgba(0,0,0,0.04);margin-bottom:10px;display:flex;align-items:center;gap:14px;text-decoration:none;color:inherit;transition:transform .15s,box-shadow .2s}
  222.         .similar-card:hover{transform:translateY(-1px);box-shadow:0 4px 20px rgba(108,58,237,.1);color:inherit}
  223.         .similar-card-logo{width:50px;height:50px;border-radius:12px;background:linear-gradient(135deg,#EDE9FE,#DDD6FE);display:flex;align-items:center;justify-content:center;color:var(--theme-color,#6C3AED);flex-shrink:0;overflow:hidden}
  224.         .similar-card-logo img{width:100%;height:100%;object-fit:cover;border-radius:8px}
  225.         .similar-card-logo svg{width:24px;height:24px;opacity:.9}
  226.         .similar-card-info{flex:1;min-width:0}
  227.         .similar-card-title{font-size:14px;font-weight:700;color:#1E1B2E;line-height:1.3;margin:0 0 4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  228.         .similar-card-meta{font-size:12px;color:#6B7280;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:flex;align-items:center;gap:8px}
  229.         .similar-card-meta-sep{display:inline-block;width:3px;height:3px;border-radius:50%;background:#D1D5DB;flex-shrink:0}
  230.         .similar-card-arrow{flex-shrink:0;color:#9CA3AF;transition:color .15s,transform .15s}
  231.         .similar-card:hover .similar-card-arrow{color:var(--theme-color,#6C3AED);transform:translateX(2px)}
  232.         .similar-card-arrow svg{width:18px;height:18px}
  233.         /* ─── Section "Offres recommandées" (cross-sell article -> jobs) ─── */
  234.         .recommended-jobs-header{
  235.             display:flex;align-items:center;justify-content:space-between;gap:10px;
  236.             margin:32px 0 12px;padding-left:4px;flex-wrap:wrap;
  237.         }
  238.         .recommended-jobs-header .similar-section-title{margin:0}
  239.         .recommended-jobs-icon{
  240.             display:inline-flex;align-items:center;justify-content:center;
  241.             width:22px;height:22px;border-radius:6px;
  242.             background:#F5F3FF;
  243.             color:var(--theme-color,#6C3AED);
  244.             flex-shrink:0;
  245.         }
  246.         .recommended-jobs-icon svg{width:11px;height:11px}
  247.         .recommended-jobs-all{
  248.             display:inline-flex;align-items:center;gap:4px;
  249.             font-size:12px;font-weight:600;
  250.             color:var(--theme-color,#6C3AED);
  251.             text-decoration:none;
  252.             padding:5px 10px;border-radius:100px;
  253.             border:1px dashed rgba(108,58,237,.35);
  254.             transition:background .15s,border-style .15s,transform .15s;
  255.         }
  256.         .recommended-jobs-all:hover{
  257.             background:#F5F3FF;
  258.             border-style:solid;
  259.             color:var(--theme-color,#6C3AED);
  260.             transform:translateX(2px);
  261.         }
  262.         .recommended-jobs-all svg{width:11px;height:11px}
  263.         /* Grille des cards jobs (style identique au composant _jobs_content.html.twig) */
  264.         .recommended-jobs-grid{margin-bottom:14px}
  265.         .recommended-jobs-grid .job-card{
  266.             display:flex;flex-direction:column;gap:.75rem;
  267.             height:100%;
  268.             padding:1.25rem;
  269.             background:#fff;
  270.             border:1px solid #e6e8ec;
  271.             border-radius:14px;
  272.             text-decoration:none;color:inherit;
  273.             transition:transform .15s ease,box-shadow .15s ease,border-color .15s ease;
  274.         }
  275.         .recommended-jobs-grid .job-card:hover{
  276.             transform:translateY(-2px);
  277.             box-shadow:0 8px 24px rgba(20,24,40,.08);
  278.             border-color:#d6d9e0;
  279.             text-decoration:none;color:inherit;
  280.         }
  281.         .recommended-jobs-grid .job-card__header{display:flex;align-items:center;gap:.75rem}
  282.         .recommended-jobs-grid .job-card__logo{
  283.             width:44px;height:44px;
  284.             border-radius:10px;
  285.             object-fit:cover;
  286.             background:#f4f5f8;
  287.             flex-shrink:0;
  288.         }
  289.         .recommended-jobs-grid .job-card__logo--placeholder{
  290.             display:flex;align-items:center;justify-content:center;
  291.             font-weight:700;color:#6b7280;
  292.             background:linear-gradient(135deg,#eef2ff,#f5f3ff);
  293.         }
  294.         .recommended-jobs-grid .job-card__company{
  295.             display:flex;flex-direction:column;
  296.             min-width:0;
  297.         }
  298.         .recommended-jobs-grid .job-card__company-name{
  299.             font-weight:600;font-size:.9rem;color:#111827;
  300.             white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
  301.         }
  302.         .recommended-jobs-grid .job-card__location{
  303.             font-size:.8rem;color:#6b7280;
  304.             display:inline-flex;align-items:center;gap:.25rem;
  305.         }
  306.         .recommended-jobs-grid .job-card__summary{
  307.             font-size:.875rem;color:#4b5563;
  308.             margin:0;line-height:1.45;
  309.             display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;
  310.             overflow:hidden;
  311.         }
  312.         .recommended-jobs-grid .job-card__footer{
  313.             display:flex;flex-wrap:wrap;gap:.375rem;
  314.             margin-top:auto;padding-top:.5rem;
  315.         }
  316.         .recommended-jobs-grid .job-card__tag{
  317.             font-size:.72rem;font-weight:500;
  318.             padding:.25rem .55rem;
  319.             border-radius:999px;
  320.             background:#f3f4f6;color:#374151;
  321.         }
  322.         .recommended-jobs-grid .job-card__tag--accent{
  323.             background:#eef2ff;color:#4338ca;
  324.         }
  325.         /* Bouton retour vers la liste */
  326.         .article-back-link{display:inline-flex;align-items:center;gap:6px;padding:8px 14px;background:#fff;border:1px solid #E5E7EB;border-radius:10px;font-size:12px;font-weight:600;color:#4B5563;text-decoration:none;margin-bottom:14px;transition:border-color .15s,color .15s}
  327.         .article-back-link:hover{border-color:var(--theme-color,#6C3AED);color:var(--theme-color,#6C3AED)}
  328.         .article-back-link svg{width:13px;height:13px}
  329.     </style>
  330. {% endblock css %}
  331. {% block body %}
  332.     <div class="article-layout">
  333.         {# ═══ Card principale : titre + meta + cover ═══ #}
  334.         <article class="article-card-main">
  335.             {# Cover (si image) #}
  336.             {% if article.image.name is not null %}
  337.                 <div class="article-card-cover">
  338.                     <img src="{{ vich_uploader_asset(article, 'imageFile') }}" alt="{{ article.title }}" />
  339.                     <div class="article-card-cover-overlay"></div>
  340.                 </div>
  341.             {% endif %}
  342.             <div class="article-card-head">
  343.                 <h1 class="article-card-title">{{ article.title }}</h1>
  344.                 {% if article.subtitle is not empty %}
  345.                     <p class="article-card-subtitle">{{ article.subtitle }}</p>
  346.                 {% endif %}
  347.                 <div class="article-card-meta">
  348.                     {% if article.author is not null and article.author is not empty %}
  349.                         <span class="article-card-meta-item">
  350.                             <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  351.                                 <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>
  352.                             </svg>
  353.                             {{ article.author }}
  354.                         </span>
  355.                     {% endif %}
  356.                     {#if article.pageslug is not empty %}
  357.                         <span class="article-card-meta-item">
  358.                             <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  359.                                 <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
  360.                             </svg>
  361.                             {{ article.pageslug }}
  362.                         </span>
  363.                     {% endif %#}
  364.                 </div>
  365.             </div>
  366.         </article>
  367.         {# ═══ Sommaire flottant en bas à gauche (FAB) ═══ #}
  368.         {% set summaryHtml = autosummary(article.description) %}
  369.         {% if summaryHtml is not empty %}
  370.             <div class="article-toc-fab" id="articleTocFab">
  371.                 {# Panneau (au-dessus du bouton, masqué par défaut) #}
  372.                 <div class="article-toc-panel" id="articleTocPanel" role="dialog" aria-label="{{ 'article.summary.title'|trans({}, 'vitrine-lexend') }}">
  373.                     <div class="article-toc-panel-header">
  374.                         <span class="article-toc-panel-icon">
  375.                             <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  376.                                 <line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/>
  377.                             </svg>
  378.                         </span>
  379.                         <span class="article-toc-panel-label">{{ 'article.summary.title'|trans({}, 'vitrine-lexend') }}</span>
  380.                         <button type="button" class="article-toc-panel-close" id="articleTocClose" aria-label="Close">
  381.                             <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
  382.                                 <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
  383.                             </svg>
  384.                         </button>
  385.                     </div>
  386.                     <div class="article-toc-panel-body">
  387.                         <div class="summary article-summary">{{ summaryHtml|raw }}</div>
  388.                         {% if is_granted('ROLE_SUPER_ADMIN') %}
  389.                             <div style="margin-top:12px;padding-top:10px;border-top:1px solid #F3F4F6;">
  390.                                 <a href="{{ path('bo_articles_edit',{'id':article.id}) }}"
  391.                                    style="font-size:11px;font-weight:600;color:#DC2626;text-transform:uppercase;letter-spacing:.06em;text-decoration:none;">
  392.                                     <i class="feather-edit-2 me-1"></i>{{ 'article.summary.edit'|trans({}, 'vitrine-lexend') }}
  393.                                 </a>
  394.                             </div>
  395.                         {% endif %}
  396.                     </div>
  397.                 </div>
  398.                 {# Bouton FAB (toujours visible) #}
  399.                 <button type="button" class="article-toc-trigger" id="articleTocTrigger" aria-expanded="false">
  400.                     <span class="article-toc-trigger-icon">
  401.                         <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
  402.                             <line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/>
  403.                         </svg>
  404.                     </span>
  405.                     <span class="article-toc-trigger-label">{{ 'article.summary.title'|trans({}, 'vitrine-lexend') }}</span>
  406.                     <span class="article-toc-trigger-count" id="articleTocCount">·</span>
  407.                 </button>
  408.             </div>
  409.             <div class="article-toc-backdrop" id="articleTocBackdrop"></div>
  410.         {% endif %}
  411.         {# ═══ Contenu de l'article ═══ #}
  412.         <div class="article-card-content">
  413.             <div class="article-card-section">
  414.                 <div id="single-post" class="post-content article-content">
  415.                     {{ contentArticle(autosummaryID(article.description))|raw }}
  416.                 </div>
  417.                 {# Tags #}
  418.                 {% if article.tags is not empty %}
  419.                     <div class="article-tags">
  420.                         {% for tag in article.tags|split(',') %}
  421.                             {% set t = tag|trim %}
  422.                             {% if t is not empty %}
  423.                                 <span class="article-tag">{{ t }}</span>
  424.                             {% endif %}
  425.                         {% endfor %}
  426.                     </div>
  427.                 {% endif %}
  428.             </div>
  429.         </div>
  430.         {# ═══ CTA Recruteur / Candidat ═══ #}
  431.         {% if getEnv("KERNEL_APPLICATION") == "whileresume" and app.request.locale in ['fr','en'] %}
  432.             <div class="article-cta">
  433.                 <span class="article-cta-eyebrow">
  434.                     <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
  435.                         <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>
  436.                     </svg>
  437.                     Whileresume
  438.                 </span>
  439.                 <h2 class="article-cta-title">
  440.                     {% if app.request.locale == 'fr' %}
  441.                         Talents et entreprises se rencontrent ici.
  442.                     {% else %}
  443.                         Where talent meets fast-growing companies.
  444.                     {% endif %}
  445.                 </h2>
  446.                 <div class="article-cta-grid">
  447.                     <div class="article-cta-side">
  448.                         <div class="article-cta-side-head">
  449.                             <span class="article-cta-side-icon">
  450.                                 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  451.                                     <rect x="2" y="7" width="20" height="14" rx="2" ry="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/>
  452.                                 </svg>
  453.                             </span>
  454.                             <span class="article-cta-side-label">{{ app.request.locale == 'fr' ? 'Recruteur' : 'Recruiter' }}</span>
  455.                         </div>
  456.                         <h3 class="article-cta-side-heading">
  457.                             {{ app.request.locale == 'fr' ? "Recrutez des profils d'exception, plus vite." : 'Hire exceptional talent, faster.' }}
  458.                         </h3>
  459.                         <p class="article-cta-side-text">
  460.                             {% if app.request.locale == 'fr' %}
  461.                                 Accédez aux meilleurs talents du marché et connectez-vous directement à des candidats qualifiés en quête de leur prochain défi.
  462.                             {% else %}
  463.                                 Get access to top market talent and connect directly with qualified candidates ready for their next challenge.
  464.                             {% endif %}
  465.                         </p>
  466.                         <a href="{{ path('whileresume_business_' ~ app.request.locale) }}" class="article-cta-side-btn">
  467.                             {{ app.request.locale == 'fr' ? 'Je recrute' : "I'm recruiting" }}
  468.                             <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
  469.                                 <line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/>
  470.                             </svg>
  471.                         </a>
  472.                     </div>
  473.                     <div class="article-cta-side">
  474.                         <div class="article-cta-side-head">
  475.                             <span class="article-cta-side-icon">
  476.                                 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  477.                                     <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>
  478.                                 </svg>
  479.                             </span>
  480.                             <span class="article-cta-side-label">{{ app.request.locale == 'fr' ? 'Candidat' : 'Candidate' }}</span>
  481.                         </div>
  482.                         <h3 class="article-cta-side-heading">
  483.                             {{ app.request.locale == 'fr' ? 'Trouvez le job qui vous ressemble.' : 'Find the job that fits you.' }}
  484.                         </h3>
  485.                         <p class="article-cta-side-text">
  486.                             {% if app.request.locale == 'fr' %}
  487.                                 On vous accompagne dans la recherche de votre poste idéal au sein des entreprises les plus prometteuses du marché.
  488.                             {% else %}
  489.                                 We support you in finding your ideal position within the most promising companies on the market.
  490.                             {% endif %}
  491.                         </p>
  492.                         <a href="{{ path('whileresume_resume_' ~ app.request.locale) }}" class="article-cta-side-btn">
  493.                             {{ app.request.locale == 'fr' ? 'Je cherche un job' : "I'm looking for a job" }}
  494.                             <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
  495.                                 <line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/>
  496.                             </svg>
  497.                         </a>
  498.                     </div>
  499.                 </div>
  500.             </div>
  501.         {% endif %}
  502.         {% if article.ctaHTML is not empty %}{{ article.ctaHTML|raw }}{% endif %}
  503.         {# ═══ Articles similaires ═══ #}
  504.         {% if similarArticles is defined and similarArticles is not empty %}
  505.             <h4 class="similar-section-title">
  506.                 {{ app.request.locale == 'fr' ? 'Articles similaires' : 'Similar articles' }} · {{ similarArticles|length }}
  507.             </h4>
  508.             {% for sa in similarArticles %}
  509.                 {# Construction de l'URL article (idem que dans list.html.twig) #}
  510.                 {% set prefix = "" %}
  511.                 {% set urlSa = path('cvs_website_article',{'slug': sa.slug}) %}
  512.                 {% if app.request.locale != default_locale %}
  513.                     {% set urlSa = path('locale_cvs_website_article',{'_locale':app.request.locale,'slug': sa.slug}) %}
  514.                     {% set prefix = "/" ~ app.request.locale %}
  515.                 {% endif %}
  516.                 {% if sa.pageslug3 is not empty %}
  517.                     {% set urlSa = prefix ~ '/' ~ sa.pageslug ~ '/' ~ sa.pageslug2 ~ '/' ~ sa.pageslug3 %}
  518.                 {% elseif sa.pageslug2 is not empty %}
  519.                     {% set urlSa = prefix ~ '/' ~ sa.pageslug ~ '/' ~ sa.pageslug2 %}
  520.                 {% elseif sa.pageslug is not empty %}
  521.                     {% set urlSa = prefix ~ '/' ~ sa.pageslug %}
  522.                 {% endif %}
  523.                 <a href="{{ urlSa }}" class="similar-card">
  524.                     <div class="similar-card-logo">
  525.                         {% if sa.image.name is not null %}
  526.                             <img src="{{ vich_uploader_asset(sa, 'imageFile') }}" alt="">
  527.                         {% else %}
  528.                             <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  529.                                 <path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/>
  530.                                 <path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>
  531.                             </svg>
  532.                         {% endif %}
  533.                     </div>
  534.                     <div class="similar-card-info">
  535.                         <div class="similar-card-title">{{ sa.title }}</div>
  536.                         <div class="similar-card-meta">
  537.                             <span>{{ sa.publishedAt|date("d M Y") }}</span>
  538.                             {% if sa.author is not null and sa.author is not empty %}
  539.                                 <span class="similar-card-meta-sep"></span>
  540.                                 <span>{{ sa.author }}</span>
  541.                             {% endif %}
  542.                             {% if sa.pageslug is not empty %}
  543.                                 <span class="similar-card-meta-sep"></span>
  544.                                 <span>{{ sa.pageslug }}</span>
  545.                             {% endif %}
  546.                         </div>
  547.                     </div>
  548.                     <span class="similar-card-arrow">
  549.                         <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  550.                             <polyline points="9 18 15 12 9 6"/>
  551.                         </svg>
  552.                     </span>
  553.                 </a>
  554.             {% endfor %}
  555.         {% endif %}
  556.         {# ═══ Jobs aléatoires (style identique au composant _jobs_content) ═══ #}
  557.         {% if recommendedJobs is defined and recommendedJobs is not empty %}
  558.             <div class="recommended-jobs-header">
  559.                 <h4 class="similar-section-title" style="margin-bottom:0">
  560.                     <span class="recommended-jobs-icon">
  561.                         <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  562.                             <rect x="2" y="7" width="20" height="14" rx="2" ry="2"/>
  563.                             <path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/>
  564.                         </svg>
  565.                     </span>
  566.                     {{ app.request.locale == 'fr' ? 'Découvrez nos offres' : 'Discover our jobs' }} · {{ recommendedJobs|length }}
  567.                 </h4>
  568.                 <a href="{% if app.request.locale == 'en' %}{{ path('whileresume_jobs_list') }}{% else %}{{ path('locale_whileresume_jobs_list',{'_locale':app.request.locale}) }}{% endif %}"
  569.                    class="recommended-jobs-all">
  570.                     {{ app.request.locale == 'fr' ? 'Voir toutes les offres' : 'See all jobs' }}
  571.                     <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
  572.                         <polyline points="9 18 15 12 9 6"/>
  573.                     </svg>
  574.                 </a>
  575.             </div>
  576.             <div class="row g-3 recommended-jobs-grid">
  577.                 {% for k in recommendedJobs %}
  578.                     <div class="col-md-6 col-lg-4">
  579.                         <a href="{% if k.locale == 'fr' %}{{ path('locale_cvs_application_job_show',{'_locale':app.request.locale,'slug':k.slug}) }}{% else %}{{ path('cvs_application_job_show',{'slug':k.slug}) }}{% endif %}" class="job-card">
  580.                             <div class="job-card__header">
  581.                                 {% if k.image and k.image.name %}
  582.                                     <img src="{{ vich_uploader_asset(k, 'imageFile') }}" alt="{{ k.companyName }}" class="job-card__logo">
  583.                                 {% else %}
  584.                                     <div class="job-card__logo job-card__logo--placeholder">
  585.                                         {{ k.companyName|default('?')|slice(0, 1)|upper }}
  586.                                     </div>
  587.                                 {% endif %}
  588.                                 <div class="job-card__company">
  589.                                     <span class="job-card__company-name">{{ k.jobTitle }}</span>
  590.                                     <span class="job-card__location">
  591.                                         <i class="fas fa-map-marker-alt"></i>
  592.                                         {{ k.city }}{% if k.country %}, {{ k.country }}{% endif %}
  593.                                     </span>
  594.                                 </div>
  595.                             </div>
  596.                             {% set summary = k.jobSummary ?: k.shortDescription %}
  597.                             {% if summary %}
  598.                                 <p class="job-card__summary">{{ summary|striptags|slice(0, 140) }}{% if summary|length > 140 %}…{% endif %}</p>
  599.                             {% endif %}
  600.                             <div class="job-card__footer">
  601.                                 {% if k.employmentType %}<span class="job-card__tag">{{ k.employmentType }}</span>{% endif %}
  602.                                 {% if k.remoteWork %}<span class="job-card__tag job-card__tag--accent">{{ k.remoteWork }}</span>{% endif %}
  603.                             </div>
  604.                         </a>
  605.                     </div>
  606.                 {% endfor %}
  607.             </div>
  608.         {% endif %}
  609.     </div>
  610. {% endblock body %}
  611. {% block footer_js %}
  612.     {{ parent() }}
  613.     <script>
  614.         (function() {
  615.             var fab       = document.getElementById('articleTocFab');
  616.             var trigger   = document.getElementById('articleTocTrigger');
  617.             var panel     = document.getElementById('articleTocPanel');
  618.             var closeBtn  = document.getElementById('articleTocClose');
  619.             var backdrop  = document.getElementById('articleTocBackdrop');
  620.             var countEl   = document.getElementById('articleTocCount');
  621.             if (!fab || !trigger) return;
  622.             // ── Ouvrir / fermer le panneau ──
  623.             function openPanel(){
  624.                 fab.classList.add('is-open');
  625.                 if (backdrop) backdrop.classList.add('is-visible');
  626.                 trigger.setAttribute('aria-expanded', 'true');
  627.             }
  628.             function closePanel(){
  629.                 fab.classList.remove('is-open');
  630.                 if (backdrop) backdrop.classList.remove('is-visible');
  631.                 trigger.setAttribute('aria-expanded', 'false');
  632.             }
  633.             function togglePanel(){
  634.                 if (fab.classList.contains('is-open')) closePanel(); else openPanel();
  635.             }
  636.             trigger.addEventListener('click', function(e){ e.stopPropagation(); togglePanel(); });
  637.             if (closeBtn) closeBtn.addEventListener('click', closePanel);
  638.             if (backdrop) backdrop.addEventListener('click', closePanel);
  639.             // Échap pour fermer
  640.             document.addEventListener('keydown', function(e){
  641.                 if (e.key === 'Escape' && fab.classList.contains('is-open')) closePanel();
  642.             });
  643.             // Clic en dehors (au cas où, pour les contextes sans backdrop)
  644.             document.addEventListener('click', function(e){
  645.                 if (!fab.classList.contains('is-open')) return;
  646.                 if (panel && !panel.contains(e.target) && !trigger.contains(e.target)) closePanel();
  647.             });
  648.             // ── Scroll spy + compteur ──
  649.             var summaryLinks = document.querySelectorAll('.article-summary a[href^="#"]');
  650.             if (countEl) {
  651.                 countEl.textContent = summaryLinks.length || '·';
  652.             }
  653.             if (!summaryLinks.length) return;
  654.             var targets = [];
  655.             summaryLinks.forEach(function(link) {
  656.                 var id = link.getAttribute('href').replace('#', '');
  657.                 var target = document.getElementById(id);
  658.                 if (target) targets.push({ link: link, target: target });
  659.             });
  660.             if (!targets.length) return;
  661.             function onScroll() {
  662.                 var scrollY = window.scrollY + 120;
  663.                 var current = null;
  664.                 var currentIndex = 0;
  665.                 targets.forEach(function(item, i) {
  666.                     if (item.target.offsetTop <= scrollY) {
  667.                         current = item;
  668.                         currentIndex = i;
  669.                     }
  670.                 });
  671.                 summaryLinks.forEach(function(l) { l.parentElement.classList.remove('active'); });
  672.                 if (current) {
  673.                     current.link.parentElement.classList.add('active');
  674.                     if (countEl) {
  675.                         countEl.textContent = (currentIndex + 1) + '/' + targets.length;
  676.                         trigger.classList.add('has-active');
  677.                     }
  678.                 } else {
  679.                     if (countEl) countEl.textContent = targets.length;
  680.                     trigger.classList.remove('has-active');
  681.                 }
  682.             }
  683.             window.addEventListener('scroll', onScroll, { passive: true });
  684.             onScroll();
  685.             // Ferme le panneau quand on clique sur un lien d'ancre
  686.             summaryLinks.forEach(function(link){
  687.                 link.addEventListener('click', function(){
  688.                     setTimeout(closePanel, 150);
  689.                 });
  690.             });
  691.         })();
  692.     </script>
  693. {% endblock footer_js %}